Проблема блокирующих вызовов

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

Модель «поток на запрос»

В servlet-приложении запрос обычно занимает worker на время обработки. Если он ждет downstream 2 секунды, этот worker 2 секунды не обслуживает другой запрос. Реальный предел определяют не только настройки HTTP-сервера, но и:

  • пул потоков;
  • connection pool базы;
  • лимиты downstream;
  • очередь входящих запросов;
  • память и deadline.

Числа вроде «Tomcat всегда имеет 200 подключений» неверны без конкретной версии и конфигурации.

Последовательное и параллельное ожидание

последовательно: A(100 ms) -> B(200 ms) -> C(150 ms) ≈ 450 ms
параллельно:     max(A, B, C) + сборка              ≈ 200 ms

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

Три модели

Ограниченный пул platform threads

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

Virtual threads (Java 21+)

Позволяют сохранить последовательный стиль при большом числе блокирующих задач. Они не увеличивают пропускную способность БД и не отменяют semaphores, connection pools, timeout и backpressure.

Неблокирующая реактивная цепочка

Небольшое число event-loop потоков обрабатывает события, если вся цепочка действительно non-blocking. Один блокирующий JDBC/файловый вызов на event loop останавливает обработку множества запросов; его нужно вынести на предназначенный пул.

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

Перегрузка и backpressure

Безграничная очередь превращает перегрузку в рост памяти и latency. Защита:

  • ограниченная очередь;
  • timeout/deadline;
  • semaphore на дорогую зависимость;
  • pagination/streaming;
  • отказ до истощения ресурсов;
  • метрики active threads, queue size, pool wait и timeout.

На собеседовании

  • Почему блокирующий I/O вреден ограниченному пулу? Поток занят ожиданием и не берет другую работу.
  • Решают ли virtual threads лимит БД? Нет.
  • Можно ли блокировать event loop? Нет, долгий вызов задержит множество операций.
  • Зачем bounded queue? Ограничить память и обнаружить перегрузку.
  • Когда параллельные downstream-вызовы хуже? Когда они зависимы или перегружают общий ресурс.

См. Реактивное программирование Java, Как выполнить 2 задачи параллельно.