← назад к разделу

Десять лет аргумент за 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 за каждое «да»:

  1. Сервис — шлюз/прокси/edge с сотнями тысяч одновременных соединений.
  2. Ядро задачи — потоки данных с backpressure, а не запрос-ответ.
  3. Весь I/O-стек уже неблокирующий (R2DBC, реактивные клиенты) — или БД в сервисе нет.
  4. Команда работала с reactive в проде и хочет продолжать.
  5. 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 — стек, который выбирается по умолчанию.
  • Монолит или микросервисы — развилка уровнем выше.