Пул потоков Tomcat: почему 200 — не магическое число
У коннектора Tomcat по умолчанию maxThreads="200", и это значение годами переезжает из проекта в проект без изменений. Обычно с ним всё в порядке — до первого инцидента, после которого его либо поднимают до тысячи, либо опускают до пятидесяти. Оба решения чаще всего принимают наугад.
Сколько потоков нужно на самом деле
Хорошая отправная точка — закон Литтла: среднее число одновременно обрабатываемых запросов равно потоку запросов, умноженному на среднее время ответа. 400 запросов в секунду при 50 мс на запрос — это 20 занятых потоков в среднем. При 500 мс — уже 200, и пул упирается в потолок.
Отсюда главный вывод: число потоков почти никогда не определяет пропускную способность само по себе. Оно определяет, сколько запросов может одновременно ждать — базу, соседний сервис, диск.
Узкое место обычно не в Tomcat
Типичная картина: maxThreads=200, а у HikariCP maximumPoolSize=10 (значение по умолчанию). Когда база начинает отвечать медленнее, 190 потоков выстраиваются в очередь за соединением, держат память и сокеты клиентов, а Tomcat выглядит «зависшим». Поднимать в этот момент maxThreads бессмысленно — очередь просто станет длиннее.
Что помогает на практике:
- задать
connectionTimeoutу пула соединений меньше, чем таймаут клиента, чтобы запрос падал быстро и понятно; - смотреть не на загрузку CPU, а на метрики пула: число активных потоков, ожидающих соединения и время ожидания;
- помнить про
acceptCount(100 по умолчанию) — это очередь соединений, которые уже приняты ядром, но ещё не получили поток.
Что меняют виртуальные потоки
В Tomcat 11 и в свежих версиях 10.1 коннектор умеет работать на виртуальных потоках Java 21:
<Connector port="8080" protocol="HTTP/1.1"
useVirtualThreads="true" />
В Spring Boot то же самое включается одной настройкой spring.threads.virtual.enabled=true. Ограничение в 200 потоков исчезает, но вместе с ним исчезает и неявная защита: раньше пул платформенных потоков сам ограничивал нагрузку на базу, теперь тысячи виртуальных потоков разом придут к тем же десяти соединениям. Лимиты приходится выставлять явно — семафором, размером пула или rate limiter'ом на входе.
Короткий вывод
Прежде чем менять maxThreads, стоит посчитать ожидаемую конкурентность по закону Литтла и сравнить её с самым узким ресурсом за приложением. Почти всегда настраивать нужно именно его.