Перейти к содержанию
V1 V2

Виртуальные потоки против реактивного: что реально меняется в архитектуре бэкенда

11 сентября 2026

Бóльшую часть последнего десятилетия обсуждения реактивного программирования и архитектур thread-per-request велись как аргументы о производительности. Одна сторона указывала на event loop'ы, неблокирующий I/O и способность обслуживать много одновременных соединений малым числом потоков операционной системы. Другая сторона указывала на простоту обычного императивного кода, знакомую отладку и стоимость превращения всего приложения в цепочки futures, publishers, колбэков или suspending-функций.

Виртуальные потоки меняют условия этого обсуждения. Они не делают реактивное программирование устаревшим и не делают асинхронный I/O ненужным. Что они делают — устраняют одну из сильнейших исторических причин выставлять асинхронное исполнение по всему обычному коду приложения: дефицит платформенных потоков.

Это различие важно. Реактивное программирование и виртуальные потоки — не два синтаксических выбора для одного и того же рантайма. Они размещают сложность в разных частях архитектуры. Реактивное приложение обычно моделирует ожидание явно в своих API и компонует асинхронные стадии как потоки данных. Приложение на виртуальных потоках может держать слой приложения синхронным, пока JVM приостанавливает и возобновляет лёгкие потоки под ним. Обе модели могут достигать высокой конкурентности, но они делают разные компромиссы в API, драйверах, распространении контекста, отладке, контроле потока, управлении ресурсами и обработке сбоев.

Kora 2 делает здесь ясный архитектурный выбор. Обращённые к приложению контракты фреймворка Kora синхронны: контроллеры, HTTP-клиенты, репозитории и запланированные задачи используют обычные сигнатуры Java или Kotlin, тогда как Kora диспетчеризует работу приложения на виртуальные потоки. Реактивные и Kotlin suspend-контракты не являются частью модулей Kora. В HTTP-сервере обработка запросов синхронна, и каждый запрос диспетчеризуется на виртуальный поток; JDBC-репозитории тоже синхронны. Смысл не в том, чтобы имитировать реактивный стек другим синтаксисом, а в возврате к прямому коду при сохранении модели конкурентности, подходящей для современных бэкенд-нагрузок.

Эта статья сравнивает два подхода как инженерные архитектуры, а не как участников бенчмарков. Интересный вопрос — не какая модель может дать наибольшее число запросов в секунду в конкретном тесте. Интересный вопрос: что меняется в форме бэкенда, когда конкурентность больше не должна представляться как асинхронный код приложения?


Историческая проблема, которую решало реактивное программирование

Традиционные Java-серверы обычно строились вокруг модели thread-per-request. Запрос входил в сервер, платформенный поток обрабатывал его, и тот же поток проходил через контроллер, сервис, слой базы данных, HTTP-клиенты и другой код приложения, пока ответ не был завершён.

Концептуально эта модель была отличной:

запрос
  |
  v
controller()
  |
  v
service()
  |
  v
repository()
  |
  v
JDBC

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

Проблема масштабируемости была в самом платформенном потоке. Платформенный поток подкреплён потоком операционной системы и поэтому относительно дорог. Если запрос проводит большую часть своей жизни в ожидании базы данных, другого HTTP-сервиса, файла или брокера, поток операционной системы всё ещё остаётся дефицитным ресурсом, привязанным к этому запросу, если приложение не переключается на асинхронный I/O.

Реактивные архитектуры атаковали ровно эту проблему. Вместо выделения одного дефицитного потока на запрос они представляли ожидание как продолжения. Небольшое число event-loop или worker-потоков могло обрабатывать много логических операций, потому что операция, которой нечего было делать, возвращала управление рантайму и возобновлялась, когда появлялись данные.

Архитектура поэтому изменилась с этой:

запрос -> поток -> блокирующий вызов -> поток ждёт -> результат -> продолжить

на нечто ближе к этому:

запрос
  -> стадия A
  -> зарегистрировать продолжение
  -> вернуть поток
  -> I/O завершён
  -> запланировать продолжение
  -> стадия B
  -> ...

Это был рациональный компромисс. Если потоки дороги, ожидание должно представляться без их удержания. Стоимость в том, что модель исполнения просачивается вверх в модель API.

Репозиторий больше не возвращает естественно User; он может возвращать Mono<User>, Uni<User>, CompletionStage<User>, Future<User> или другой асинхронный тип. Сервис компонует этот тип. Контроллер компонует ещё одну стадию поверх. Контекст должен переживать переключения планировщика. Распространение исключений становится частью асинхронной абстракции. Доступ к базе данных часто переходит с JDBC на реактивный драйвер, чтобы вся цепочка могла оставаться неблокирующей.

Реактивное программирование поэтому решило реальную системную проблему, но сделало это, сделав конкурентность частью контракта приложения.


Что меняют виртуальные потоки

Виртуальный поток — всё ещё Java Thread, но он не постоянно привязан к потоку операционной системы. JVM планирует много виртуальных потоков на гораздо меньшее множество платформенных потоков, обычно называемых потоками-носителями. Когда виртуальный поток достигает поддерживаемой блокирующей операции, такой как блокирующий I/O, JVM может приостановить виртуальный поток и освободить его носитель для выполнения другого виртуального потока.

Это меняет экономику ожидания. Ожидание всё ещё существует. Задержка базы данных всё ещё существует. Сетевая задержка всё ещё существует. У приложения всё ещё есть десятки тысяч операций, которые могут одновременно ожидать внешние системы. Что становится дешёвым — представление каждой ожидающей операции как собственного потока.

Важная ментальная модель поэтому такова:

логический запрос = виртуальный поток

Виртуальный поток A ---- выполняется ----- ожидает JDBC ........ выполняется ---- готово
                         |                                 |
                         v                                 v
                    Носитель #1                        Носитель #3

Виртуальный поток B -------- выполняется ---- ожидает HTTP ........ выполняется --- готово
                           |
                           v
                      Носитель #2

Виртуальный поток — логическая единица исполнения. Носитель — ресурс реализации, используемый только пока виртуальному потоку реально нужен CPU или он иначе смонтирован. Ожидающий виртуальный поток обычно не должен монополизировать свой носитель.

Именно поэтому виртуальные потоки восстанавливают жизнеспособность архитектуры thread-per-request, не возвращаясь просто к старой модели стоимости платформенных потоков. Приложение может снова сказать:

User user = repository.findById(id);
RiskScore score = riskClient.calculate(user);
return mapper.toResponse(user, score);
val user = repository.findById(id)
val score = riskClient.calculate(user)
return mapper.toResponse(user, score)

вместо кодирования каждого ожидания в асинхронном типе:

return repository.findById(id)
    .flatMap(user -> riskClient.calculate(user)
        .map(score -> mapper.toResponse(user, score)));
return repository.findById(id)
    .flatMap { user -> riskClient.calculate(user)
        .map { score -> mapper.toResponse(user, score) } }

Различие не в том, что первая программа «блокирует всё». Она блокирует виртуальный поток, представляющий эту операцию. Когда JVM может размонтировать этот виртуальный поток во время I/O, носитель остаётся доступным для другой работы.

Виртуальные потоки поэтому разделяют две концепции, которые часто были связаны в старых Java-архитектурах:

  • синхронная семантика приложения;
  • дорогое занятие потока операционной системы.

Реактивное программирование избегало дорогого занятия потоков, меняя семантику приложения. Виртуальные потоки атакуют проблему стоимости потоков ниже в стеке.


Архитектурное сравнение с первого взгляда

Полезное сравнение — не «блокирующий против неблокирующего» как лозунг. Это где появляется семантика конкурентности и кто должен ею управлять.

Забота Реактивная архитектура Архитектура виртуальных потоков
API приложения Асинхронные значения и потоковые типы Обычные синхронные значения
Модель запроса Продолжение / конвейер Поток на запрос или задачу
Стек вызовов Разделён на асинхронных границах Обычный стек вызовов Java/Kotlin
Ожидание Представлено асинхронными стадиями Представлено припаркованным виртуальным потоком
Использование потоков Малые event-loop/worker-пулы Много ВП, мультиплексированных поверх потоков-носителей
База данных Обычно реактивный/неблокирующий драйвер JDBC вписывается естественно
HTTP-клиенты Асинхронные/реактивные клиенты вписываются естественно Блокирующий фасад вписывается естественно
Контекст Обычно явно распространяется через стадии/планировщики Может следовать за областью исполнения; Kora использует ScopedValue для контекста телеметрии
Исключения Реактивный канал ошибок / упавшая стадия Обычная семантика throw/catch
Отмена Обычно часть асинхронной/потоковой композиции Прерывание потока / структурированная отмена задач / явные API
Противодавление Первоклассный потоковый протокол Должно обеспечиваться явными ограничениями ресурсов и контролем допуска
Отладка Требует понимания операторов и границ планировщика Знакомые шаги, стеки, дампы потоков
Работа, привязанная к CPU Должна быть убрана с event loop'а Всё ещё должна быть ограничена; ВП не создают больше CPU
Потоковые конвейеры Естественное соответствие Возможно, но контроль потока не автоматически потоковый протокол
Требование экосистемы Лучше всего, когда цепочка неблокирующая из конца в конец Работает естественно с блокирующими JVM-библиотеками

Таблица сразу показывает, почему решение влияет на архитектуру далеко за пределами HTTP-сервера. Если HTTP-эндпоинт реактивен, но API базы данных блокирующий, приложению нужен мост. Если база данных реактивна, но бизнес-логика предполагает контекст, привязанный к потоку, контекст нужно мостить. Если библиотека выполняет долгую блокирующую работу на event loop'е, она может повредить весь рантайм event loop'а. Модель конкурентности становится наиболее согласованной, когда она простирается через весь стек.


Форма API: конкурентность в системе типов против конкурентности в рантайме

Самое видимое различие — сигнатура метода.

Реактивный API выставляет факт, что результат ещё не доступен:

Mono<Order> findOrder(long id);
fun findOrder(id: Long): Mono<Order>

или:

CompletionStage<Order> findOrder(long id);
fun findOrder(id: Long): CompletionStage<Order>

API, ориентированный на виртуальные потоки, может выставлять бизнес-контракт напрямую:

Order findOrder(long id);
fun findOrder(id: Long): Order

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

В реактивной архитектуре это распространение желательно, потому что защищает неблокирующую модель исполнения. Случайная блокировка потока event loop'а опасна, поэтому асинхронные типы сообщают важное ограничение исполнения.

В архитектуре виртуальных потоков то же распространение часто ненужно. Рантайм уже имеет недорогой способ представить ожидающую операцию. Сигнатура метода может поэтому описывать домен, а не планировщик.

Это одно из самых больших архитектурных следствий Loom: конкурентность может уйти из обычных интерфейсов сервисов обратно в рантайм.

Это не означает, что асинхронные API исчезают полностью. Если операция по своей природе асинхронна — потоки событий, колбэки от внешних систем, одновременный fan-out, фоновые рабочие процессы или долгоживущие подписки — асинхронная абстракция всё ещё может быть корректной моделью домена. Смысл в том, что простому вызову базы данных типа запрос/ответ больше не нужно становиться асинхронным лишь для сохранения масштабируемости сервера.


Стеки вызовов и отладка

Синхронный стек вызовов — больше, чем удобство отладки. Это архитектурное представление причинности.

Рассмотрим запрос, проходящий через пять слоёв:

OrderController.get()
  -> OrderService.load()
      -> PricingService.price()
          -> CustomerRepository.find()
              -> JDBC-драйвер

С синхронным кодом стек рантайма естественно захватывает эту цепочку. Исключение внизу может нести стек-трейс, указывающий через те же методы, которые разработчик читает в исходном коде.

Реактивное исполнение часто ломает этот физический стек на асинхронных границах. Логическая операция всё ещё имеет причинность, но стек JVM может содержать текущую механику оператора, а не весь бизнес-путь, который к нему привёл. Современные реактивные библиотеки существенно улучшили диагностику с помощью assembly tracing, контрольных точек, отладки операторов и контекстно-зависимого инструментария, но разработчику всё ещё приходится понимать различие между логическим потоком и текущим исполняемым стеком.

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

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

Это не делает системы на виртуальных потоках автоматически лёгкими для отладки. Дедлоки, истощение ресурсов, исчерпание пулов соединений, чрезмерная конкурентность, закреплённые носители и плохие политики таймаутов остаются реальными проблемами. Что меняется — представление исполнения ближе к потоку управления на уровне исходного кода.


Распространение контекста: тонкое различие

Распространение контекста часто представляется слишком упрощённо как «реактивное требует явного контекста, потоки используют ThreadLocal». Современная Java делает ситуацию более нюансированной.

Реактивные конвейеры часто пересекают потоки или планировщики. Значение, хранимое в традиционном ThreadLocal, нельзя считать следующим за логической операцией, потому что разные стадии могут выполняться на разных потоках. Реактивные библиотеки поэтому предоставляют явные механизмы контекста или интеграции, которые захватывают и восстанавливают телеметрию, безопасность, логирование или метаданные запроса.

Виртуальный поток даёт каждой логической задаче идентичность потока, поэтому обычные поток-ориентированные API становятся проще в использовании. Однако фреймворк всё равно должен решать, как должен представляться контекст запроса, наследоваться, ограничиваться и очищаться. Слепое помещение больших изменяемых структур в ThreadLocal просто потому, что виртуальные потоки — потоки, не является хорошей архитектурой.

Kora 2 даёт полезный пример. Его интеграция трассировки хранит текущий контекст OpenTelemetry через Java ScopedValue, а не традиционный ThreadLocal. Этот контекст доступен внутри динамической области, которую Kora устанавливает для операции, включая виртуальные потоки. Если код приложения явно передаёт работу другому несвязанному потоку или исполнителю, он всё равно должен распространять или перепривязывать этот контекст.

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

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


Архитектура базы данных: реактивные драйверы против JDBC

Доступ к базе данных — одно из мест, где различие становится конкретным.

В реактивном стеке использование JDBC напрямую с event loop'а обычно некорректно, потому что JDBC — блокирующий API. Медленный запрос, получение соединения, сетевой простой или пауза базы данных заблокируют поток event loop'а и помешают прогрессу несвязанных запросов, назначенных на тот же loop. Реактивные фреймворки поэтому используют реактивные драйверы баз данных или диспетчеризуют блокирующую работу JDBC в отдельный worker-пул.

Это создаёт две согласованные реактивные стратегии:

A. полностью реактивная
HTTP event loop
  -> реактивный сервис
  -> реактивный драйвер БД
  -> продолжение

B. реактивный край + блокирующий остров
HTTP event loop
  -> worker-пул
  -> JDBC
  -> продолжение event loop'а

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

Архитектура виртуальных потоков может использовать JDBC напрямую из виртуального потока запроса:

HTTP-транспорт
  -> виртуальный поток запроса
      -> контроллер
          -> сервис
              -> репозиторий
                  -> JDBC

Метод JDBC остаётся блокирующим с точки зрения приложения. Виртуальный поток ждёт. Носитель обычно может использоваться в другом месте, пока виртуальный поток припаркован на поддерживаемом I/O.

Kora 2 принимает эту форму напрямую. Его JDBC-контракты репозиториев синхронны, и его документация явно утверждает, что нет асинхронных или реактивных сигнатур репозиториев. Генерируемая реализация репозитория поэтому может выглядеть и вести себя как обычный код JDBC, при этом вписываясь в высоко конкурентную архитектуру сервера.

Это имеет архитектурные следствия за пределами читаемости. JDBC — одна из самых глубоких и зрелых экосистем в Java. Пулы соединений, наблюдаемость баз данных, драйверы, семантика транзакций, возможности вендоров, инструментарий и операционные знания обильны. Фреймворк, который может сохранить JDBC, не выделяя один платформенный поток на ожидающий запрос, может переиспользовать эту экосистему вместо требования параллельного реактивного стека баз данных.

Однако виртуальные потоки не делают базу данных бесконечно конкурентной. У базы данных всё ещё конечные соединения, CPU, блокировки, память, пропускная способность I/O и ёмкость транзакций. Пул Hikari с 20 соединениями всё ещё позволяет примерно 20 одновременно активных сессий базы данных независимо от того, ждут ли их 100 или 100 000 виртуальных потоков.

Это важный сдвиг в мышлении: потоки перестают быть главным ограничителем конкурентности, поэтому реальные ресурсы должны стать ограничителями конкурентности.


Противодавление: где у реактивного всё ещё есть реальное структурное преимущество

Противодавление — сильнейшая область, где реактивное программирование остаётся концептуально другим, а не просто более сложным.

Reactive Streams был спроектирован вокруг асинхронной потоковой обработки с неблокирующим противодавлением. Потребитель сообщает, сколько данных он готов принять, и спрос распространяется через поток. Это не просто «ограничение конкурентности». Это протокол координации производителей и потребителей через асинхронные границы.

Представь производителя, способного испускать миллион событий в секунду, и потребителя, способного обрабатывать только сто тысяч. Без контроля потока разница должна где-то накапливаться: в памяти, внутренней очереди, брокере, буфере сокета, потерянных сообщениях или увеличенной задержке. Реактивный поток может сделать спрос частью самой композиции.

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

Система на виртуальных потоках поэтому нуждается в явных контролях ёмкости, таких как:

  • ограниченные пулы соединений базы данных;
  • семафоры вокруг дефицитных нижестоящих сервисов;
  • ограниченные очереди;
  • ограничители скорости;
  • контроль допуска на HTTP- или messaging-границах;
  • ограничения конкурентности на клиента;
  • ограничения потребителей брокера;
  • явные размеры потоковых окон.

Это не обязательно хуже. Для систем запрос/ответ эти ограничения ресурсов часто напрямую соответствуют реальному узкому месту и могут быть проще для рассуждений, чем сквозной реактивный граф. Если партнёрский сервис разрешает 50 одновременных вызовов, семафор на 50 точно выражает правило.

Но для непрерывных потоков, графов, управляемых спросом, или многостадийных конвейеров, где скорость производства должна динамически адаптироваться к скорости потребления, Reactive Streams предоставляет более богатую нативную модель. Это одно из мест, где утверждение «виртуальные потоки заменяют реактивное» было бы технически неверным.


Event loop'ы и правило, которое не меняется

Виртуальные потоки не делают блокировку безопасной на event loop'е.

Это различие важно, потому что такой сервер, как Undertow, всё ещё использует потоки сетевого I/O. Эти потоки отвечают за перемещение байтов, принятие соединений, разбор протокольных событий и эффективное управление сетевым слоем. Они дефицитны по дизайну.

Kora 2 поэтому разделяет транспорт и исполнение приложения. Undertow имеет настраиваемое число потоков сетевого I/O, но Kora диспетчеризует обработку запросов на виртуальные потоки. Код приложения затем может блокироваться на JDBC или других поддерживаемых блокирующих API, не блокируя сетевой поток.

Архитектура примерно такова:

сокет
  |
  v
поток I/O Undertow / XNIO
  |
  | диспетчеризация
  v
виртуальный поток
  |
  +--> контроллер
  +--> сервис
  +--> JDBC / блокирующий HTTP-клиент / файловая система
  +--> маппинг ответа
  |
  v
Undertow пишет ответ

Важное правило остаётся: не выполняй произвольную блокирующую работу приложения на event loop'е.

Фреймворк с приоритетом виртуальных потоков не отменяет event loop'ы; он убирает код приложения с них. Event loop остаётся специализированным для сети, тогда как виртуальный поток становится единицей исполнения приложения.

Это более чистое разделение обязанностей, чем трактовка самого event loop'а как места, где должна выполняться произвольная бизнес-логика.


Работа, привязанная к CPU: ни одна модель не создаёт больше ядер

Обе архитектуры иногда обсуждаются так, будто их механизмы конкурентности улучшают все нагрузки. Это не так.

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

Реактивное программирование имеет то же фундаментальное ограничение. Перенос CPU-тяжёлого кода через операторы map на event loop'е может быть даже опаснее, потому что долгое вычисление монополизирует поток event loop'а и задерживает много несвязанных операций. Реактивные фреймворки поэтому обычно требуют переноса CPU-тяжёлой или блокирующей работы на подходящий планировщик или worker-пул.

Правильная архитектура в любой модели — ограничивать конкурентность, интенсивную по CPU, близко к фактической параллельной ёмкости CPU.

Для виртуальных потоков это может означать семафор, выделенный ограниченный исполнитель для специализированной нагрузки или ограничение конкурентности на уровне сервиса. Для реактивных систем это обычно означает переключение с планировщика event loop'а на ограниченный worker-планировщик.

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

Этот принцип централен для их корректного использования.


Пулы ресурсов становятся важнее, а не менее

В старой модели платформенных потоков пул из 200 потоков запросов случайно действовал как глобальный ограничитель конкурентности. Даже если никто не проектировал его как противодавление, только 200 запросов могли быть активно представлены этими воркерами одновременно. Всё остальное стояло в очереди до входа в приложение.

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

Предположим, сервис может создать 50 000 виртуальных потоков, но имеет:

  • 30 соединений базы данных;
  • 100 разрешённых одновременных вызовов к платёжному провайдеру;
  • продюсера Kafka с конечными буферами;
  • четыре ядра CPU для шифрования;
  • 2 ГиБ кучи, доступных для тел запросов.

Приложение — не система с 50 000-кратной конкурентностью в каком-либо значимом сквозном смысле. Это система с несколькими разными ограничениями ёмкости.

Хорошая архитектура виртуальных потоков поэтому заменяет общий тюнинг пулов потоков ресурсо-ориентированными ограничениями:

Конкурентность HTTP: потенциально большая
Конкурентность БД: 30
Конкурентность платёжного API: 100
Конкурентность шифрования: ~размер CPU
Большие тела запросов: явно ограничены
Глубины очередей: ограничены и наблюдаемы

Это обычно более правдивая модель системы.

Реактивные архитектуры тоже нуждаются в той же модели ёмкости, но их механизмы исполнения часто встраивают части контроля потока в publishers, спрос подписчиков, конкурентность операторов и очереди планировщиков. Системы на виртуальных потоках склонны выражать эти ограничения знакомыми конструкциями синхронизации и пулов ресурсов.

Ни один подход не убирает инженерию ёмкости. Они делают её видимой в разных местах.


Обработка сбоев и исключения

Обработка ошибок — ещё одно место, где простота на уровне исходного кода имеет операционные следствия.

В синхронном Java или Kotlin операция может бросить исключение. Вызывающий может его поймать. Перехватчик транзакций может откатиться вокруг вызова. Аспект ретрая может вызвать метод снова. Отладчик может остановиться в месте броска.

Реактивные системы представляют сбой как часть асинхронного протокола. Вместо throw конвейер может произвести сигнал ошибки или упавший future. Восстановление становится оператором:

source
    .flatMap(this::callRemote)
    .retryWhen(...)
    .onErrorResume(...)
source
    .flatMap(this::callRemote)
    .retryWhen(...)
    .onErrorResume(...)

Эта модель мощна. Она особенно выразительна для потоков, потому что ошибки, завершение, отмена и спрос — все часть жизненного цикла потока. Но она также означает, что каждая интеграция фреймворка должна понимать этот жизненный цикл. Транзакция не может просто закончиться, когда метод возвращается, если возвращённый publisher ещё не выполнился. Спан трассировки не может просто закрыться на выходе из метода, если асинхронная операция всё ещё выполняется. Ресурс не может быть освобождён лишь потому, что объект publisher был создан.

Именно поэтому реактивные интеграции часто имеют специализированные операторы транзакций, хуки контекста, инструментарий трассировки и обёртки с учётом жизненного цикла.

С синхронным кодом виртуальных потоков время жизни метода и время жизни операции обычно снова совпадают. Это значительно упрощает сквозной код фреймворка.

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


Структурированная конкурентность меняет историю fan-out

Одно историческое преимущество асинхронной композиции — удобный fan-out. Если запрос должен вызвать три независимых сервиса, асинхронный API может запустить все три операции и объединить их результаты без выделения трёх платформенных потоков.

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

Концептуально:

виртуальный поток запроса
   |
   +---- дочерний ВП -> сервис ценообразования
   |
   +---- дочерний ВП -> сервис инвентаря
   |
   +---- дочерний ВП -> сервис рекомендаций
   |
   +---- join дочерних
   |
   v
компоновать ответ

Это локализует конкурентность в точке, где бизнес-операции реально нужна конкурентность.

Kora 2 явно рекомендует структурированную конкурентность Java для операций, которым нужно выполнять несколько действий параллельно. На момент его текущей документации StructuredTaskScope всё ещё preview API, поэтому конфигурация сборки и рантайма должна это учитывать. Архитектурное направление тем не менее важно: обычный последовательный код по умолчанию, явный структурированный параллелизм там, где нужно.

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

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

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


Закрепление и потоки-носители: что «блокировка дёшева» не означает

Виртуальные потоки делают многие блокирующие операции дешёвыми, но фразу не следует интерпретировать как «каждая блокирующая операция бесплатна».

Виртуальный поток выполняется на потоке-носителе, пока он исполняется. Когда он встречает блокирующую операцию, которую JVM умеет обрабатывать, он может размонтировать виртуальный поток, чтобы носитель был доступен для другого виртуального потока. Если виртуальный поток не может размонтироваться, он остаётся закреплённым на носителе, пока заблокирован.

Современные релизы JDK снизили важные источники закрепления, но нативные или foreign-function вызовы всё ещё могут закреплять виртуальный поток. Долгоживущее закрепление важно, потому что потоки-носители намеренно ограничены. Если много виртуальных потоков одновременно закрепляют носители, ожидая, архитектура начинает терять преимущество масштабируемости, которое делало виртуальные потоки привлекательными.

Это ведёт к практическому правилу: приложениям на виртуальных потоках всё ещё нужна производственная диагностика вокруг насыщения носителей, закрепления, нижестоящей задержки и пиков конкурентности.

Правильное сравнение поэтому не:

реактивное = неблокирующее
виртуальные потоки = блокирующее

Оно ближе к:

реактивное:
приложение явно представляет ожидания как асинхронные стадии

виртуальные потоки:
приложение использует блокирующую семантику;
JVM пытается превращать ожидания в приостановку виртуального потока

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


Память и форма конкурентности

Реактивные системы часто хвалят за малое число потоков, тогда как системы на виртуальных потоках могут иметь огромное число потоков. Смотреть только на число потоков вводит в заблуждение, потому что единицы фундаментально разные.

Реактивная система представляет ожидающую работу в объектах publisher, колбэках, состоянии продолжений, очередях, буферах и графах операторов. Система на виртуальных потоках представляет большую часть этого ожидающего состояния в стеках виртуальных потоков и метаданных планировщика.

Обеим моделям нужна память для незавершённой работы. Ни одна не может принять бесконечную конкурентность без последствий.

Это важно, когда сервис получает пик. Если прибывает 500 000 запросов и каждый запрос немедленно создаёт виртуальный поток, система может избежать исчерпания платформенных потоков, но всё равно может исчерпать кучу, сокеты, буферы тел запросов, ожидающих базы данных или нижестоящую ёмкость. Реактивные системы сталкиваются с той же базовой проблемой, если вышестоящий производитель обгоняет нижестоящий спрос или если в конвейер вводится неограниченная буферизация.

Важное архитектурное свойство поэтому — ограниченность, а не число потоков.

Спроси:

  • сколько запросов может быть допущено?
  • сколько памяти может удерживать каждая ожидающая операция?
  • какие очереди ограничены?
  • какие удалённые системы ограничивают конкурентность?
  • где перегрузка отклоняется?
  • как быстро распространяется отмена?

Виртуальные потоки делают один ресурс — потоки — гораздо дешевле. Они не отменяют теорию очередей.


Отмена и таймауты

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

В синхронной модели поток-на-задачу отмена обычно представляется прерыванием потока, отменой в области задачи, явными API таймаутов, отменой сокета или фреймворк-специфичной отменой запроса. Это может быть проще для изолированных вызовов запрос/ответ, но менее композиционно для сложных потоков.

Архитектурное требование одинаково в обоих случаях: отмена должна достигать дорогой операции.

Таймаут, который лишь перестаёт ждать на внешнем HTTP-слое, оставляя запрос базы данных, HTTP-вызов или фоновое вычисление выполняться, — не реальная отмена ресурсов. Реактивное программирование не гарантирует корректную отмену автоматически, и виртуальные потоки не гарантируют её через прерывание автоматически; драйверы и библиотеки должны сотрудничать.

Для обычных вызовов сервисов структурированная конкурентность плюс ограниченные таймауты могут давать ясную модель владения: дочерняя работа принадлежит запросу и отменяется, когда область падает или истекает таймаут. Для долгоживущих потоков реактивная семантика отмены часто более естественна, потому что отмена уже встроена в потоковый протокол.


Транзакции становятся проще в прямом коде

Транзакции иллюстрируют, как стиль конкурентности просачивается в дизайн фреймворка.

В синхронном коде транзакция часто может моделироваться лексически:

return transactionManager.inTx(() -> {
    var account = accounts.find(id);
    ledger.insert(...);
    accounts.update(...);
    return account;
});
return transactionManager.inTx {
    val account = accounts.find(id)
    ledger.insert(...)
    accounts.update(...)
    account
}

Транзакция начинается до колбэка, остаётся активной, пока выполняется колбэк, и завершается, когда колбэк возвращается или бросает исключение.

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

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

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


Наблюдаемость: логические операции против механизмов исполнения

Обе модели могут быть полностью наблюдаемы с OpenTelemetry, но они требуют разных стратегий инструментария.

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

С виртуальными потоками логическая операция естественно имеет идентичность потока, но контекст телеметрии всё равно нуждается в дисциплинированной области. Kora 2 использует контекст OpenTelemetry на основе ScopedValue, чтобы Span.current() и Context.current() работали внутри управляемых фреймворком операций, включая виртуальные потоки. Когда разработчики явно отправляют отсоединённую работу другому исполнителю, они должны захватывать и перепривязывать контекст.

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

Операционно виртуальные потоки также улучшают полезность традиционных инструментов. Дамп потоков может представлять логические задачи, отладчик может шагать через обычные фреймы, и Java Flight Recorder может выставлять события виртуальных потоков, такие как запуски, завершения, отправки и проблемное закрепление.

У реактивных систем тоже отличный инструментарий наблюдаемости, но команды должны учиться интерпретировать графы операторов, имена планировщиков, assembly traces и асинхронные причинные цепочки. Стоимость не обязательно производительность рантайма; это операционный словарь и когнитивная нагрузка.


Где реактивное программирование всё ещё объективно полезно

Появление виртуальных потоков не устраняет нагрузки, для которых был спроектирован Reactive Streams. Есть несколько случаев, где реактивное остаётся объективно сильным архитектурным соответствием.

1 Непрерывные потоки, управляемые спросом

Если приложение обрабатывает текущий поток, чей размер неизвестен или фактически бесконечен, противодавление — не случайная деталь. Это часть проблемного домена.

Примеры включают:

  • потоки рыночных данных;
  • конвейеры телеметрии;
  • потоки change-data-capture;
  • большие графы обработки событий;
  • непрерывно преобразуемые потоки брокера;
  • медиа- или бинарный стриминг;
  • конвейеры, которые джойнят, буферизуют, окончат, дебаунсят, сэмплируют или сливают живые источники.

Reactive Streams даёт этим системам стандартный словарь для спроса, отмены, завершения и ошибок. Виртуальный поток, безусловно, может читать и обрабатывать поток, но одна семантика поток-на-задачу не предоставляет эквивалентный протокол спроса.

2 Сквозные реактивные экосистемы

Если каждая важная зависимость уже выставляет реактивные интерфейсы, принудительное возвращение приложения в синхронные обёртки может добавить больше сложности, чем убрать.

Система, построенная вокруг реактивных драйверов баз данных, реактивных API обмена сообщениями, реактивного HTTP-стека и библиотек, чьи примитивы композиции — publishers, может быть наиболее согласованной, когда остаётся реактивной из конца в конец.

Архитектура должна минимизировать семантические адаптеры. Преобразование publishers в блокирующие вызовы лишь для того, чтобы заявить о синхронной архитектуре, не лучше по своей сути.

3 Движки преобразования потоков с высокой пропускной способностью

Некоторые приложения ближе к движкам потоков данных, чем к сервисам запрос/ответ. Их естественная абстракция — не «один поток обрабатывает один запрос»; это «элементы текут через граф операторов».

Для этих систем операторы, такие как merge, zip, window, buffer, throttle, sample, retry, concat и switch, могут выражать домен более прямо, чем императивные циклы и вручную координируемые задачи.

4 Сетевые шлюзы и протокольные прокси

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

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

5 Мелкозернистое распространение спроса через много стадий

Если нижестоящая стадия должна динамически сообщать нескольким вышестоящим стадиям, сколько ещё работы она может принять, Reactive Streams уже определяет механизм. Ручное пересоздание эквивалентного поведения семафорами и очередями может давать менее композиционную систему.

6 Существующая экспертиза и зрелые производственные системы

Архитектура имеет стоимость миграции. Зрелый реактивный сервис с проверенными библиотеками, диагностикой, операционным инструментарием и командой, свободно владеющей моделью, не следует переписывать лишь потому, что существуют виртуальные потоки.

Виртуальные потоки меняют выбор по умолчанию для новых сервисов запрос/ответ сильнее, чем оправдывают переписывание здоровых реактивных систем.


Где виртуальные потоки обычно более простой выбор по умолчанию

Виртуальные потоки особенно убедительны, когда приложение — обычный бэкенд-сервис, чья работа в основном запрос/ответ и привязана к I/O:

  • HTTP CRUD-сервисы;
  • бизнес-API;
  • сервисы, где доминирует JDBC;
  • оркестрационные сервисы, вызывающие несколько нижестоящих HTTP/gRPC-систем;
  • приложения, использующие зрелые блокирующие Java-библиотеки;
  • транзакционные сервисы;
  • приложения, где отлаживаемость и простой поток управления — высокие приоритеты;
  • команды, которым в остальном не нужна потоковая семантика.

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

Это категория, вокруг которой спроектирован Kora 2.

Его HTTP-обработчики запросов используют прямые синхронные сигнатуры. Обработка запросов размещается на виртуальных потоках. JDBC-репозитории остаются синхронными. HTTP, доступ к данным, отказоустойчивость, телеметрия и другие функции фреймворка спроектированы вокруг той же прямой модели программирования. Вместо поддержки нескольких конкурирующих стилей конкурентности Kora выбирает одну модель приложения и делает фреймворк ответственным за её эффективное исполнение.

Эта согласованность важнее того, короче ли какой-то отдельный API на одну строку.


Kora 2 как конкретная архитектура с приоритетом виртуальных потоков

Архитектуру Kora 2 можно резюмировать как разделение между сетевым рантаймом и рантаймом приложения.

На транспортном слое Undertow/XNIO использует небольшое число потоков I/O. Эти потоки должны оставаться сосредоточенными на сети. Kora диспетчеризует обработку запросов приложения на виртуальный поток. С этой точки контроллеры, код сервисов, репозитории, мапперы, перехватчики и блокирующие клиенты могут выполняться с использованием прямой синхронной семантики.

Упрощённый путь запроса выглядит так:

Сокет
  |
  v
поток I/O Undertow / XNIO
  |
  | диспетчеризация
  v
виртуальный поток запроса
  |
  v
генерируемый HTTP-обработчик Kora
  |
  v
Контроллер
  |
  v
Сервис
  |
  +------> JDBC-репозиторий
  |
  +------> блокирующая HTTP/gRPC-интеграция
  |
  +------> отказоустойчивость / телеметрия / валидация
  |
  v
Ответ

Несколько выборов дизайна усиливают модель.

Во-первых, обработчики контроллеров возвращают обычные значения, а не CompletionStage, Future или реактивные типы Publisher. Во-вторых, JDBC-контракты репозиториев синхронны и не содержат реактивных сигнатур. В-третьих, общая документация Kora явно утверждает, что его код приложения выполняется синхронно на виртуальных потоках и что модули фреймворка не выставляют реактивные или suspend контракты. В-четвёртых, контекст телеметрии интегрирован с ScopedValue, поэтому синхронная область исполнения остаётся наблюдаемой без опоры на традиционную идентичность платформенного потока.

Результат — не просто «блокирующий Kora». Это фреймворк, где блокировка — семантика уровня приложения, а парковка виртуального потока — оптимизация уровня рантайма.

Это различие — ядро архитектуры.


Реактивное и виртуальные потоки могут сосуществовать — но границы важны

Реальные системы редко идеологически чисты. Сервис на виртуальных потоках может потреблять реактивную библиотеку. Реактивное приложение может нуждаться в блокирующем SDK. Оба возможны.

Опасность — неконтролируемое смешение.

Реактивное вызывает блокирующий код

Если реактивный код вызывает блокирующий код на event loop'е, event loop может остановиться. Блокирующая работа должна быть перенесена на worker-планировщик или другой механизм исполнения.

event loop
  -> диспетчеризация на worker / ВП
      -> блокирующий SDK
  -> продолжить реактивный конвейер

Это легитимная граница, но она должна быть явной и наблюдаемой.

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

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

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

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


Реальный компромисс: явный поток управления против явного контроля потока

Самое глубокое сравнение можно выразить как компромисс между двумя видами явности.

Архитектуры виртуальных потоков делают поток управления явным:

var user = users.find(id);
var balance = balances.get(user.accountId());
var risk = riskClient.check(user);
return buildResponse(user, balance, risk);
val user = users.find(id)
val balance = balances.get(user.accountId)
val risk = riskClient.check(user)
return buildResponse(user, balance, risk)

Порядок операций виден напрямую в языке.

Реактивные архитектуры делают контроль потока явным:

users.find(id)
    .flatMap(user ->
        Mono.zip(
            balances.get(user.accountId()),
            riskClient.check(user)
        ).map(tuple -> buildResponse(user, tuple.getT1(), tuple.getT2()))
    );
users.find(id)
    .flatMap { user ->
        Mono.zip(
            balances.get(user.accountId),
            riskClient.check(user)
        ).map { tuple -> buildResponse(user, tuple.t1, tuple.t2) }
    }

Конвейер явно моделирует асинхронную композицию и может естественно нести спрос и отмену.

Для обычных бизнес-сервисов поток управления обычно более важная абстракция. Для потоковых систем контроль потока может быть более важной абстракцией.

Это лучшая основа для выбора архитектуры, чем «реактивное быстрее» или «виртуальные потоки проще».


Рамка решений для бэкенд-команд

При выборе между реактивной архитектурой и архитектурой с приоритетом виртуальных потоков задавай вопросы о нагрузке, а не о моде стека.

Выбирай виртуальные потоки как выбор по умолчанию, когда:

  1. Сервис в основном запрос/ответ.
  2. Большинство ожиданий — JDBC, HTTP, RPC, файловая система или другой блокирующий стиль I/O.
  3. Домен естественно выражается обычными вызовами методов.
  4. Противодавление в основном контроль конкурентности на уровне ресурсов, а не распространение спроса на уровне потоков.
  5. Ты выигрываешь от зрелой блокирующей Java-экосистемы.
  6. Транзакции и распространение исключений должны оставаться лексическими и прямыми.
  7. Важны отладка, стек-трейсы и операционная простота.
  8. Команда хочет, чтобы конкурентность была явной только там, где реально требуется параллелизм.

Предпочитай реактивное, когда:

  1. Домен — текущий поток, а не конечный запрос.
  2. Распространение спроса — первоклассное требование.
  3. Приложение уже имеет сквозную реактивную цепочку зависимостей.
  4. Потоковые преобразования на основе операторов напрямую отображаются на бизнес-проблему.
  5. Отмена должна распространяться через долгоживущие асинхронные конвейеры.
  6. Система фундаментально конечный автомат обработки событий или сети.
  7. Команда уже успешно эксплуатирует реактивную модель, и переписывание принесло бы мало архитектурной выгоды.

Будь осторожен с обеими моделями, когда:

  1. Нагрузка привязана к CPU.
  2. Конкурентность неограничена.
  3. Ёмкость нижестоящих систем не моделируется явно.
  4. Таймауты существуют только на самом внешнем слое.
  5. Очереди и буферы неограничены.
  6. Контекст исчезает, когда работа передаётся отсоединённым исполнителям.
  7. Блокирующий код случайно выполняется на сетевых event loop'ах.
  8. Закрепление виртуальных потоков или истощение планировщика реактивного не мониторится.

Это проблемы системного дизайна, а не проблемы стиля API.


Что не меняется после Loom

Виртуальные потоки — крупное изменение JVM, но многие правила инженерии бэкенда остаются ровно теми же.

Тебе всё ещё нужны разумные таймауты. Тебе всё ещё нужны ограниченные очереди. Тебе всё ещё нужны ограничения соединений базы данных. Тебе всё ещё нужны bulkhead'ы вокруг хрупких нижестоящих систем. Тебе всё ещё нужно защищать CPU-тяжёлую работу. Тебе всё ещё нужна отмена. Тебе всё ещё нужно поведение при перегрузке. Тебе всё ещё нужны метрики вокруг задержки и насыщения. Тебе всё ещё нужно понимать, что твои библиотеки делают под нагрузкой.

Что меняется — поток больше не должен трактоваться как дефицитный ресурс, вокруг которого спроектирована вся архитектура приложения.

Это глубокий сдвиг, потому что большая часть сложности реактивных бизнес-приложений происходила от сохранения дефицитных потоков через ожидания I/O. Когда лёгкий поток может представлять ожидающий запрос напрямую, архитектура может вернуть многие заботы на уровень, где они естественно принадлежат:

  • ёмкость базы данных контролируется пулом базы данных;
  • нижестоящая конкурентность контролируется на нижестоящей границе;
  • работа CPU контролируется ёмкостью CPU;
  • исполнение запроса представляется потоком запроса;
  • параллелизм вводится там, где бизнес-операция реально параллельна;
  • стриминг использует потоковые абстракции там, где стриминг реально является доменом.

Модель конкурентности становится менее глобальной.


Заключение

Реактивное программирование не было ошибкой. Оно решило реальное ограничение JVM до Loom: большое число одновременно ожидающих операций нельзя было экономично представить большим числом платформенных потоков. Реактивный ответ заключался в кодировании ожидания, продолжения, отмены и спроса в асинхронные API и потоковую композицию.

Виртуальные потоки решают большую часть той же проблемы масштабируемости на другом слое. Вместо требования от каждого API приложения выставлять асинхронное исполнение JVM может приостановить лёгкий поток, пока он ждёт, и переиспользовать носитель для другой работы. Это делает прямой синхронный код снова жизнеспособным для высоко конкурентных сервисов, привязанных к I/O.

Архитектурное следствие больше, чем замена Mono<T> на T.

Оно меняет дизайн репозиториев. Оно меняет границы транзакций. Оно меняет стек-трейсы. Оно меняет распространение контекста. Оно меняет то, как реализуются аспекты фреймворка. Оно меняет, где живут ограничения ёмкости. Оно меняет, как команды отлаживают производственные сбои. Самое главное, оно позволяет представлению конкурентности перестать доминировать в обычных бизнес-API.

Kora 2 доводит эту идею до логического завершения. Его контракты фреймворка синхронны, HTTP-работа приложения выполняется на виртуальных потоках, JDBC остаётся первоклассной прямой моделью базы данных, а реактивные или suspend-сигнатуры намеренно отсутствуют в модулях Kora. Сетевой слой может оставаться асинхронным там, где асинхронная сеть полезна, тогда как слой приложения остаётся синхронным там, где полезен прямой поток управления.

Реактивное программирование всё ещё имеет сильное место: непрерывные потоки, конвейеры, управляемые спросом, графы обработки событий, долгоживущие асинхронные потоки и системы, чья экосистема уже реактивна из конца в конец. В этих доменах противодавление и асинхронная композиция — не обходные пути реализации; это полезные абстракции сами по себе.

Практический вывод поэтому не «виртуальные потоки бьют реактивное». Он точнее:

Виртуальные потоки устраняют необходимость делать обычный бизнес-код запрос/ответ реактивным лишь для достижения высокой конкурентности I/O. Реактивное программирование остаётся ценным, когда сама проблема реактивна.

Это различие, вероятно, определит следующее поколение архитектуры JVM-бэкенда точнее, чем любой график бенчмарков.