Гайд по 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.