Spring WebFlux vs Spring MVC с виртуальными потоками: выбор подхода

Оглавление

  1. Введение
  2. Обзор Spring WebFlux
  3. Обзор Spring MVC с Virtual Threads (JDK 21+)
  4. Сравнение подходов
  5. Как выбрать стек: рекомендации
  6. Часто задаваемые вопросы (FAQ)
  7. Заключение

Введение: проблема конкурентности

Современные микросервисные системы обслуживают тысячи одновременных запросов, чаще всего – I/O-интенсивных (вызовы к БД, API, файловым системам и т.п.). Классическая модель «один поток на запрос» больше не масштабируется: тысячи ОС-потоков перегружают CPU и память. Поэтому появились два подхода:

  • Spring WebFlux – реактивный неблокирующий стек с минимальным числом потоков (основан на Project Reactor).
  • Spring MVC + Virtual Threads (VT) – классическая модель с лёгкими потоками, управляемыми JVM (проект Loom в JDK 21).

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

Обзор: как работают подходы

Spring WebFlux (Reactor Netty)

  • Обрабатывает запросы асинхронно через event-loop.
  • Использует Mono, Flux – реактивные типы.
  • Работает на Netty или асинхронных сервлетах (Servlet 3.1+).
  • Минимальное потребление потоков, хорош для I/O-bound задач.

Плюсы:

  • Высокая масштабируемость.
  • Поддержка стриминга: WebSocket, SSE, RSocket.
  • Эффективность при больших нагрузках.

Минусы:

  • Требует знания реактивной модели.
  • Сложнее отладка и трассировка.
  • Проблемы при работе с blocking API (например, JDBC).

Spring MVC с Virtual Threads (JDK 21+)

  • Каждый запрос получает свой виртуальный поток (легкий, управляется JVM).
  • Работает с привычными API: JdbcTemplate, RestTemplate, @Transactional.
  • Запускается на Tomcat/Jetty с spring.threads.virtual.enabled=true.

Плюсы:

  • Императивный стиль кода.
  • Совместимость со всей экосистемой Spring.
  • Безболезненная миграция старых приложений.
  • Простота отладки, tracing и использования ThreadLocal.

Минусы:

  • Возможны проблемы с pinned threads (например, native-код).
  • Все еще зависит от CPU – если поток работает, он занимает ядро.
  • Чуть выше накладные расходы при больших нагрузках, чем у WebFlux.

Сравнение подходов

КритерийWebFlux (Reactor)Spring MVC + VT
Модель выполненияАсинхронная (event loop)Поток-на-запрос (VT)
Стиль кодаФункциональный, реактивИмперативный, привычный
Совместимость с blocking APIТребуются обходыРаботает из коробки
Использование ресурсовМинимум потоковБольше, но дёшево
Кривая обученияВысокаяНизкая
Поддержка backpressureДаНет
Стриминг (WebSocket/SSE)Отлично подходитТребует обвязки
Отладка и логгированиеСложно (контекст теряется)Привычные инструменты
Поддержка ThreadLocal, MDCСложноДа
Совместим с Micrometer/OpenTelemetryСложнееДа

Как выбрать стек: рекомендации

Рекомендуется использовать WebFlux, если:

  • Требуется экстремальная I/O масштабируемость.
  • Используются WebSocket, SSE, RSocket.
  • Все сервисы уже на реактивных драйверах (R2DBC, WebClient).
  • Вы разрабатываете Gateway/API-Proxy.
  • Критичны latency и количество потоков.

Рекомендуется использовать Spring MVC + Virtual Threads, если:

  • Основной код синхронный (JDBC, JPA, RestTemplate).
  • Требуется сохранить императивный стиль.
  • Команда не знакома с реактивностью.
  • Проект мигрирует с классического Spring MVC.
  • Вы хотите масштабировать старое приложение без глобального рефакторинга.

Комбинированный подход

Многие проекты комбинируют оба подхода:

КомпонентРекомендуемый стек
Gateway/EdgeWebFlux + Netty
Бизнес-логикаSpring MVC + VT
Реалтайм потокWebFlux + SSE/WebSocket
БДVT + JDBC / Hibernate

Часто задаваемые вопросы (FAQ)

1. WebFlux быстрее Virtual Threads?

Иногда.
WebFlux эффективен при I/O-нагрузке и стриминге. Virtual Threads ближе к нему по производительности, но могут проигрывать в CPU-биндинге или при большом числе pinned-потоков.

2. Virtual Threads могут полностью заменить WebFlux?

Нет.
WebFlux нужен там, где важны backpressure, push-механизмы (WebSocket, SSE), реактивные стримы. VT – хорошее решение для миграции и “простых” сервисов.

3. Можно ли комбинировать Web и WebFlux в одном приложении?

Да.
Spring Boot позволяет это. Но контроллеры должны быть изолированы – например, с помощью аннотаций @RestController с разными base path (/api/* и /r/*) или запускаться по разным профилям.

4. Virtual Threads требуют перехода на R2DBC?

Нет.
Именно наоборот: их преимущество – полная совместимость с JDBC и другими блокирующими API.

5. Нужно ли переписывать код под Virtual Threads?

Нет.
Сохраняется обычный Spring MVC-код. Просто включается флаг spring.threads.virtual.enabled=true.

6. Могу ли я использовать @Async с Virtual Threads?

Да.
Начиная с Spring Boot 3.2, дефолтный SimpleAsyncTaskExecutor использует Thread.ofVirtual().factory() – т.е. @Async автоматически работает на VT.

7. Работают ли MDC, ThreadLocal и Tracing с Virtual Threads?

Да.
Полная поддержка, как с обычными потоками. Это одно из больших преимуществ VT над WebFlux.

8. Есть ли ограничения у Virtual Threads в Spring?

Да.

  • pinned threads (например, native-вызовы через JNI)
  • ошибки в старых пулях потоков (Tomcat < 10.1.12)
  • не все сторонние библиотеки оптимизированы под Loom

9. Можно ли использовать WebClient в MVC с виртуальными потоками?

Да.
WebClient работает нормально, но не даёт выигрыша в latency, так как вызывается из виртуального потока и сам – реактивный. Это допустимо, но не “обязательно”.

10. Что выбрать для бэка офиса или админки?

MVC + Virtual Threads.
Там нет стриминга, мало параллелизма – важна простота кода и совместимость. VT отлично справятся.

11. Работает ли Spring Security с Virtual Threads?

Да.
Spring Security 6 полностью совместим с VT. Контекст авторизации доступен как обычно.

12. Поддерживаются ли Virtual Threads в тестах (JUnit)?

Да.
JUnit 5.10+ позволяет запускать тесты на виртуальных потоках через настройку @Execution(ExecutionMode.CONCURRENT) + custom Executor.

13. Можно ли использовать VT в GraalVM native-image?

Пока нет.
GraalVM не поддерживает Loom в native-image (по состоянию на 2025). Рекомендуется использовать обычные потоки.

14. Как логгировать traceId с Virtual Threads?

Через MDC.
Работает как с обычными потоками, если использовать Spring Sleuth или Micrometer Tracing. Главное – правильно инициализировать контекст.

15. Что будет, если вызвать blocking API внутри WebFlux?

Произойдёт блокировка event-loop и падение производительности.
Для этого WebFlux предлагает Schedulers.boundedElastic(), но лучше избегать таких вызовов.

Заключение

Spring WebFlux и Spring MVC с Virtual Threads – два мощных подхода к масштабированию приложений. Первый – для реактивного, потокового и ресурсоэффективного I/O. Второй – для простоты, совместимости и быстрого старта на Java 21+. В ближайшие годы они будут сосуществовать, и выбор между ними должен опираться на:

  • Характер нагрузки
  • Уровень зрелости команды
  • Требования к производительности и latency
  • Готовность к смене архитектурного стека