Project Loom и Spring Boot: тесты производительности

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Сегодня я хочу выяснить, готов ли Project Loom заменить Spring WebFlux при создании высоконагруженных приложений с высокой пропускной способностью.

Проблемы реактивного подхода

WebFlux - замечательная технология с фантастической производительностью, однако:

  • При использовании реактивного подхода код сложнее писать и сопровождать

  • Стектрейсы малополезны при разборе ошибок

  • Все связанные клиенты/библиотеки также должны быть написаны в реактивном стиле

Что такое Project Loom:

  • В статусе превью-фичи с Java 19, разработка стартовала в 2017

  • Основное нововведение - виртуальные потоки, призванные значительно снизить трудозатраты на написание и сопровождение приложений

  • Предполагается возможность создавать миллионы виртуальных потоков. (Прим. пер. мне кажется, автору стоило явно выделить основную мысль. Поскольку виртуальные потоки дешевы, то запуск блокирующего кода в таком потоке тоже дешевая операция, т.к. реальный системный поток при этом не блокируется и может заняться чем-нибудь полезным )

  • Предполагается минимальное вмешательство в существующий код

  • Стек виртуальных потоков хранится в хипе JVM

    Маппинг множества виртуальных потоков на ограниченное множество системных
    Маппинг множества виртуальных потоков на ограниченное множество системных

Есть мнение, что Project Loom способен решить проблемы применения реактивной парадигмы. Но что насчет производительности?

Тестовый сценарий

Мы собираемся проверить производительность сервиса, выполняющего задачу проксирования запроса к некоему третьему сервису, который возвращает ответ с задержкой в 500мс. Исходный код здесь.

Мы проверим 3 реализации одного и того же сервиса:

  • Spring Boot (Tomcat) + Project Loom

  • Spring Webflux

  • Spring Webflux + Project Loom

Железо:

  • Все тесты крутятся на серверах AWS EC2

  • Подопытный сервис крутится на ноде t2.micro (1 CPU, 1 GB)

  • Третий сервис крутится на ноде t2.medium (2 CPU, 4GB)

  • Нагрузка создается еще одним внешним EC2

Tomcat + Loom

Необходимо кастомизировать настройки Spring Boot, чтобы Tomcat использовал виртуальные потоки вместо своего стандартного пула. Далее используем обычный контроллер Spring MVC

@Configuration 
public class Config {    
  @Bean    
  AsyncTaskExecutor applicationTaskExecutor() { 
    // enable async servlet support        
    ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();        
    return new TaskExecutorAdapter(executorService);    
  }    
  
  @Bean    
  TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {        
    return protocolHandler -> protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());    
  }
}

@RestController
public class Controller {    
  private final RestTemplate restTemplate = new RestTemplate();   
  private final String host = "http://test:7000/address/";  
  
  @GetMapping("/address/{timeout}")    
  String getAddress(@PathVariable long timeout) throws URISyntaxException { 
    URI uri = new URI(host + timeout);        
    return restTemplate.getForObject(uri, String.class);    
  }
}

WebFlux

Для реализации с WebFlux я использую http-клиент WebClient. Стандартные настройки немного изменены для поддержки большого числа подключений.

@RestController
public class Controller {
  
  private final WebClient webClient = init();
  private final String host = "http://test:7000/address/";
  
  private WebClient init() {        
    String connectionProviderName = "myConnectionProvider";        
    HttpClient httpClient = HttpClient.create(ConnectionProvider.builder(connectionProviderName)                
        .maxConnections(10_000)                
        .pendingAcquireMaxCount(10_000)                
        .pendingAcquireTimeout(Duration.of(100, ChronoUnit.SECONDS))                
        .build()        
      );
    return WebClient.builder()                
      .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
  }
  
  private Mono<String> getAddressInternal(long timeout) {        
    return webClient.get()                
      .uri(host + timeout)                
      .exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class))                
      .timeout(Duration.ofSeconds(200));    
  }
  
  @GetMapping("/address-reactive/{timeout}")    
  Mono<String> getAddress(@PathVariable long timeout) {        
    return getAddressInternal(timeout);    
  }
}

WebFlux + Project Loom

Здесь мы будем вызывать некий блокирующий код, но на пуле виртуальных потоков Executors.newVirtualThreadPerTaskExecutor(). Результат вызова блокирующего кода оборачиваем в Mono.

@GetMapping("/address-loom/{timeout}")
Mono<String> getAddressWithLoom(@PathVariable long timeout) {   
  return Mono.fromFuture(
    CompletableFuture
      .supplyAsync(() -> 
                   getAddressInternal(timeout).block(), 
                   Executors.newVirtualThreadPerTaskExecutor()
  ));
}

Результаты

Tomcat + Loom показывает неудовлетворительные результаты (Прим. пер. Есть основания полагать, что "просто" Tomcat не показал бы даже и таких). Пропускная способность невысока из-за высокой активности GC (~50CPU).

Tomcat +Loom для 4k параллельных запросов
Tomcat +Loom для 4k параллельных запросов

Связка Tomcat + Loom неспособна справиться с нагрузкой в 4k параллельных запросов, а для 8k запросов уже происходит OOM.

После анализа heap dump ясно, что почти вся память занята инстансами SocketWrapper, созданными Tomcat. Это легко объяснить, т.к. дизайн Tomcat предполагает модель "1 запрос - 1 поток". Поэтому обертки сокетов слишком "тяжелы", и использование их в связке с виртуальными потокам неэффективно.

Tomcat +Loom: heap dump report
Tomcat +Loom: heap dump report

Сравним профили WebFlux и WebFlux + Loom

Webflux: 8k параллельных запросов
Webflux: 8k параллельных запросов
Webflux+ Loom: 8k параллельных запросов
Webflux+ Loom: 8k параллельных запросов

Профили нагрузки на память, CPU и GC похожи. Поэтому мы наблюдаем похожую пропускную способность, хотя для 10k запрос Loom даже вырывается вперед.

Итог

Project Loom это "game changer". Мы показали, что виртуальные потоки эффективны, и позволяют писать простой привычный блокирующий код, который может быть столь же , как код реактивный/неблокирующий. Это означает, что мы сможем легко мигрировать наш блокирующий код на Loom и продолжать использовать код нереактивных библиотек типа Hibernate. Но мы все еще нуждаемся в связке Netty+WebFlux в качестве обертки для блокирующего кода, т.к. Tomcat пока что by-design не подходит для этой задачи.

P.S. Ограничения Project Loom

Системный поток все же может быть заблокирован, если внутри виртуального потока есть:

  • Вызовы нативного кода

  • Синхронизированный участок кода/метод. Решение: использовать ReentrantLock и -Djdk.tracePinnedThreads=full

Это означает, что существующие библиотека должны быть отрефакторены с заменой ключевого слова synchronized на ReentrantLock. См. https://github.com/pgjdbc/pgjdbc/issues/1951

Источник: https://habr.com/ru/articles/745748/


Интересные статьи

Интересные статьи

В первой части статьи мы рассказали, как легко и быстро построить инфраструктуру для запуска UI-тестов на Android с помощью Appium и Selenoid. Продолжаем историю и рассказываем, как внедрили в схему з...
В этом сезоне Heisenbug пройдёт хитрее обычного, совместив онлайн и офлайн. На рубеже весны–лета мы проведём онлайн-часть конференции. А когда станет еще теплее — можно будет встретиться в Питере и за...
При разработке на Spring Boot иногда нам нужно выполнить метод или фрагмент кода при запуске приложения. Этот код может быть любым, от записи определенной информации до настройки базы данных, заданий ...
Да, вы не ослышались – именно так! В IT-сообществе прочно укоренилось мнение, что все эти тесты вам хоть как-то помогают, но так ли это на самом деле? Вы сами пробовали м...
Новая игрушка Мы продолжаем знакомится с новым материалом от Apple, представленным на WWDC. На этот раз рассмотрим MetricKit, это абсолютно новый фреймворк, который служит инструментом для м...