Десять лет аргумент за WebFlux звучал так: thread-per-request не масштабируется — потоки дорогие, и тысячи висящих в ожидании БД запросов съедают память. Виртуальные потоки Java 21 закрыли именно этот аргумент: теперь и MVC может держать десятки тысяч ожидающих запросов на дешёвых потоках. Развилка не исчезла — но сместилась, и большинство старых ответов устарело.
Три варианта на 2026 год
- MVC на платформенных потоках — классика: пул в пару сотен потоков, каждый запрос занимает поток целиком, включая ожидание I/O.
- MVC на виртуальных потоках — та же модель программирования (обычный блокирующий код, обычные стектрейсы, обычный
ThreadLocal), но поток стоит килобайты, и их можно иметь миллионы. В Spring Boot 3.2+ — один флаг:spring.threads.virtual.enabled: true. - WebFlux — реактивный конвейер:
Mono/Flux, неблокирующий I/O сверху донизу, backpressure. Другая модель программирования, отладки и тестирования (разбор).
Ключевой сдвиг: дефолт сместился к «MVC + виртуальные потоки». WebFlux из «как выдержать нагрузку» превратился в инструмент для специфических задач потоковой обработки.
Шесть критериев
1. Профиль нагрузки
MVC + VT покрывает типичный сервис: много конкурентных запросов, каждый из которых в основном ждёт БД и соседей. Именно этот профиль раньше гнал в WebFlux.
WebFlux сохраняет преимущество там, где счёт соединений на сотни тысяч и важен контроль над каждым байтом буферов — шлюзы, прокси, edge-сервисы.
2. Потоковая семантика
MVC + VT: запрос-ответ; SSE и стриминг есть, но без backpressure.
WebFlux: бесконечные потоки данных, медленные клиенты, необходимость притормаживать продюсера — Flux с backpressure это моделирует, виртуальные потоки — нет. Тест: есть ли в задаче слово «поток данных», а не «запрос»?
3. Модель программирования и команда
MVC + VT: обычный Java-код — читается, отлаживается, профилируется стандартными средствами; новичок продуктивен с первого дня.
WebFlux: операторные цепочки, контекст вместо ThreadLocal, реактивная отладка. Команда либо уже умеет, либо заплатит месяцами обучения и классом багов, которых в блокирующем коде не бывает.
4. Экосистема зависимостей
MVC + VT: JDBC, jOOQ, любые блокирующие SDK работают как есть. Единственная категория проблем — synchronized-блокировки вокруг I/O в старых библиотеках (pinning), и эта категория тает с каждым релизом JDK.
WebFlux: выигрывает только при неблокирующем стеке сверху донизу — R2DBC вместо JDBC, реактивные клиенты ко всему. Один блокирующий вызов в цепочке (block(), JDBC, тяжёлый маппер) — и event loop встал; реактивность не деградирует частично, она ломается.
5. Latency-профиль
MVC + VT: латентность определяется I/O, как и раньше; виртуальные потоки не делают запрос быстрее — они делают дешёвым ожидание.
WebFlux: то же самое. Бенчмарки «WebFlux быстрее» почти всегда меряют throughput при экстремальной конкуренции, а не латентность вашего запроса. Скорость — не критерий этой развилки.
6. Существующий код
MVC + VT: миграция с классического MVC — конфигурационный флаг и аудит synchronized в горячих путях.
WebFlux: миграция с MVC — переписывание контроллеров, сервисов и тестов. В обратную сторону — так же. Это решение уровня «новый сервис», а не «переключим».
Чек-лист «возьми балл»
Балл WebFlux за каждое «да»:
- Сервис — шлюз/прокси/edge с сотнями тысяч одновременных соединений.
- Ядро задачи — потоки данных с backpressure, а не запрос-ответ.
- Весь I/O-стек уже неблокирующий (R2DBC, реактивные клиенты) — или БД в сервисе нет.
- Команда работала с reactive в проде и хочет продолжать.
- SLA требует контроля над буферами и поведением при медленных потребителях.
0–1 — MVC + виртуальные потоки, и это дефолт без оправданий. 2–3 — пограничье: чаще побеждает MVC + VT с точечным WebClient там, где нужен стриминг. 4+ — WebFlux осознанно, со всей дисциплиной реактивного стека.
Типичные ошибки
- WebFlux «для производительности». Сервис на 200 RPS с JDBC внутри переписывают на реактивщину «чтобы быстрее». Латентность не меняется (она в БД), сложность утраивается, в цепочке находится
block()— и становится хуже, чем было. block()внутри реактивной цепочки. Блокирующий вызов на event loop — деградация всего сервиса под нагрузкой, а не одного запроса. Если без блокирующих зависимостей не обойтись — это аргумент против WebFlux, а не за костыльsubscribeOn.- Смешанный сервис «MVC + немного Flux». Половина контроллеров реактивные, половина нет, команда поддерживает обе модели и обе хуже среднего. Один сервис — одна модель.
- Виртуальные потоки как магия throughput. Включили флаг — ждут ускорения CPU-bound кода. VT удешевляют ожидание; вычисления как занимали ядра, так и занимают.
- Игнорировать pinning. Включили VT при старой библиотеке пулов с
synchronizedвокруг I/O — потоки-носители заблокированы, профит исчез. Перед включением — прогон под нагрузкой с-Djdk.tracePinnedThreads.
Когда оба — нормально
Реже, чем в других развилках: внутри одного сервиса смешение моделей — ошибка (см. выше). Нормально на уровне ландшафта: edge-шлюз на WebFlux, доменные сервисы на MVC + VT. И один легальный гибрид внутри MVC-сервиса — реактивный WebClient для конкретного стримингового вызова, обёрнутый на границе в блокирующий вид.
Что почитать дальше
- Spring WebFlux: когда брать, Mono/Flux, R2DBC — устройство реактивного стека и его ловушки.
- Scheduled, Async, виртуальные потоки — что виртуальные потоки меняют за пределами веб-слоя.
- Spring MVC — стек, который выбирается по умолчанию.
- Монолит или микросервисы — развилка уровнем выше.