Блог создается при поддержке Axiom JDK и Failover Bar

Гайд по Spring Boot — Как включить Virtual Threads (Виртуальные потоки)

December 14, 2023 . 5 минут на чтение статьи

Введение

Virtual Threads (виртуальные потоки) — это новый крутой способ снизить нагрузку на сервер, появившийся в Java 21. Эта технология подобна корутинам в Kotlin или горутинам в Go.

Это работает так. Когда программа делает блокирующий вызов, виртуальный поток "замораживается" и освобождает ресурсы операционной системы до тех пор, пока блокирующий вызов не завершит свою работу.

В качестве примера блокирующего вызова можно вспомнить любой запрос ко внешней базе данных (например, PostgreSQL), или чтение файла с файловой системы.

Получается, что много потоков могут совместно использовать одни и те же вычислительные мощности. От этого приложения выдерживают большую нагрузку: там где раньше ваше Spring Boot приложение справлялось с 500 пользователями, при включении Virtual Threads оно сможет выдержать тысячу.

Эта возможность практически бесплатно достается всем, кто обновился до Java 21. Как ее включить, и как посмотреть на результаты? Это мы обсудим в следующих двух разделах этой статьи.

Как включить виртуальные потоки

Чтобы Virtual Threads заработали, достаточно использовать Java 21 и свежую версию Spring Boot 3.2.

Добавьте в application.properties новую настройку: spring.threads.virtual.enabled=true. Это всё, что нужно для их включения. Эта настройка будет действовать как для встроенного Tomcat, так и для Jetty.

Если вы используете более старую версию Spring Boot (например, 3.1), то вы можете попробовать вручную создать следующую конфигурацию, которая будет работать вместе со встроенным Tomcat:

@EnableAsync
@Configuration
public class VirtualThreadConfig {
    
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(
    Executors.newVirtualThreadPerTaskExecutor());
}

@Bean
public TomcatProtocolHandlerCustomizer<?> 
protocolHandlerVirtualThreadExecutorCustomizer() {
    
return protocolHandler -> {
protocolHandler.setExecutor(
    Executors.newVirtualThreadPerTaskExecutor());
};
}

}

Виртуальные треды не заменяют вообще все обычные треды магическим образом. Эти настройки имеют значение для асинхронной обработки в рамках Spring MVC и Spring Web Flux, потому что фреймворк специально адаптирован для Virtual Threads. Например, они включаются для обработки @Async-методов при включенной конфигурации @EnableAsync.

Производительность виртуальных потоков

Улучшается ли производительность от использования виртуальных потоков? Стоит ли включать виртуальные потоки? Насколько хорошего результата можно достичь?

Есть простой бенчмарк, который можно взять с GitHub, и запустить проверку с помощью mvn clean install.

Этот бенчмарк запускает простое приложение для Spring Boot 3.2 и Java 21, главный контроллер которого на каждый запрос 300 миллисекунд и отдает ID текущего треда:

public class VirtualThreadController {
    
private static final Logger LOGGER = LoggerFactory
            .getLogger(VirtualThreadController.class);
public static final int SLEEP_TIME = 300;

@GetMapping("/")
public String getResponse(){
    
try {
    TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
    LOGGER.error(e.getMessage());
}

long threadId = Thread
        .currentThread()
        .threadId() ;
return  String.valueOf(threadId);
}

}

Полный текст файла: VirtualThreadController.java

Производительность приложения измеряется с помощью сценария на Gatling, записнного на языке Scala:

class VirtualThreadSimulation extends Simulation {

before {
val app = SpringApplication.run(
  classOf[VirtualThreadsApplication])
app.registerShutdownHook()
}

val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
.contentTypeHeader("application/json")

val vtScenario = scenario("VTS").repeat(1000) {
exec(http("Call the Controller")
  .get("/")
  .check(status.is(200)))
}

setUp(
vtScenario.inject(atOnceUsers(500))
).protocols(httpProtocol)

}

Полный текст файла: VirtualThreadSimulation.java

В этом сценарии мы симулируем ситуацию, когда 500 пользователей одновременно по тысяче раз выполняют запрос к контроллеру.

Выбрать, какую конфигурацию виртуальных тредов использовать, можно прямо в application.properties. Первая из этих настроек отвечает за включение обычной конфигурации из Spring Boot. Вторая позволяет подключить наш бин со вручную настроенным AsyncTaskExecutor.

spring.threads.virtual.enabled=true
spring.threads.virtual.enabled.manually=false

Полный текст файла: application.properties

Сейчас разницы между этими двумя опциями нет, но в будущем конфигурация внутри Spring Boot может разойтись со старым способом ручной настройки.

Результаты отображаются в виде красивого отчета в формате HTML, который можно посмотреть в браузере:

По этим отчетам хорошо видно, что результаты сильно зависят от используемого оборудования.

Поэтому, этот тест можно использовать как основу для экспериментов. Эти цифры можно увеличивать и уменьшать, в зависимости от мощности вашего компьютера: чтобы наблюдать интересные эффекты, для мощного 64-ядерного серверного процессора понадобится большая нагрузка, чем для слабого 4-ядерного ноутбука.

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

Здесь важно, что этот проект-пример является минимальным кодом, запускающим Gatling на вашем проекте. То есть, вы можете один в один перенести конфигурацию в свой проект и запустить тесты уже на вашем реальном проекте.

Лицензии на проект-пример

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

Если вы уже обожглись о копирование чужого кода с GitHub и волнуетесь за лицензии, то обратите внимание на файл LICENSE. Проект-пример опубликован под сверх-открытой пермиссивной лицензией Universal Public License (UPL), которая в каком-то смысле еще более опенсорсная, чем MIT и Apache 2. Там нет вирусности (как в GPL) и вам дается ничем не ограниченный доступ к патентам (которого не дает MIT/BSD, и которых там не используется).

Если даже в этом случае вас все еще мучает паранойя. Я как автор кода обещаю не подавать на вас в суд за то, что вы скопировали эти три строчки кода, не имеющие, на самом деле, никакой особой интеллектуальной ценности.

Выводы

Нагрузка в 500 пользователей и 1000 запросов изначально проверялась на процессоре AMD Ryzen 9 3950X 16-Core Processor, с 8 гигабайтами свободной оперативной памяти, на операционной системе Windows. Разница между обычными и виртуальными тредами составила примерно 2 раза.

Мы всего лишь переключили одну настройки в Spring Boot, и "бесплатно" получили статистически значимое увеличение производительности приложения. Кажтеся, это достаточно хороший результат.



Не забывайте подписаться на наши ресурсы, там есть ништяки:

  • CodCraft - Youtube-канал от автора этого гайда
  • Оправдания от Олега - Telegram-чат автора (общий, про всё на свете)
  • Javawatch - Telegram-канал про Java
  • Telegram-канал Failover Bar - единственный в Санкт-Петербурге (а может, и в России вообще) бар для разработчиков. Мы здесь постоянно встречаемся и разговариваем про Java.