Проблема блокирующих вызовов
Блокирующий вызов удерживает поток, пока ждет сеть, диск, базу данных, 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 задачи параллельно.