Tomcat глазами разработчика

Оглавление

  1. Введение
  2. Архитектура Apache Tomcat
  3. Жизненный цикл HTTP-запроса
  4. Встроенное использование в Spring Boot
  5. Ключевые задачи, решаемые Tomcat
  6. Особенности Tomcat 10 (Jakarta EE 9/10)
  7. Виртуальные потоки (Project Loom) и Tomcat 10
  8. Наблюдаемость и метрики Tomcat
  9. Сравнение с другими серверами: Jetty, Undertow, Netty
  10. Подводные камни и практические советы
  11. Заключение

Введение

Apache Tomcat – это популярный открытый Java-сервер (Servlet-контейнер), реализующий спецификации Jakarta Servlet, JSP, EL и WebSocket. Проект Tomcat был создан в Sun Microsystems и передан в Apache в 1999 году. За годы развития Tomcat стал де-факто стандартом для запуска веб-приложений на Java, будучи референсной реализацией сервлет-спецификации. Tomcat славится своей надежностью, высокой производительностью и активным сообществом.

Основное предназначение Tomcat – исполнять Java-сервлеты и JSP-страницы, динамически генерируя веб-контент. В отличие от полноценных Java EE серверов приложений, Tomcat – “легковесный” контейнер сервлетов, не включающий, к примеру, EJB или JMS. Это обеспечивает более малое потребление ресурсов и простоту администрирования. Тем не менее, он поддерживает все необходимое для веб-приложений: HTTP/HTTPS, управление потоками, подключение к внешнему веб-серверу (через AJP), кластеризацию, JDBC-пулы и т.д. Благодаря сочетанию производительности и соответствия спецификациям, Tomcat широко применяется как в небольших проектах, так и на предприятиях.

В данной статье мы подробно разберем архитектуру Apache Tomcat 10, его внутреннее устройство и жизненный цикл запросов, особенности интеграции с Spring Boot (embedded Tomcat), ключевые механизмы (управление потоками, очереди, безопасность, асинхронность), новшества Tomcat 10 (переход на Jakarta EE и пр.), виртуальные потоки Java (Project Loom) в контексте Tomcat, способы мониторинга и сравнение с альтернативными серверами (Jetty, Undertow, Netty).

Архитектура Apache Tomcat

Apache Tomcat имеет модульную архитектуру контейнеров, конфигурация которых задается в server.xml. В иерархии компонентов можно выделить следующие основные элементы:

  • Server – верхний уровень, представляющий весь экземпляр Tomcat. Обычно единственный на JVM. Именно <Server> запускается и слушает служебный порт завершения (по умолчанию 8005).
  • Service – логический сервис, объединяющий один Engine и набор Connector-ов. Service связывает обработчик запросов с сетевыми соединениями. Чаще всего используется один сервис с именем “Catalina”.
  • Connector – сетевой коннектор (на основе компонента Coyote), который слушает определенный протокол и порт (например, HTTP/1.1 на 8080, HTTPS на 8443, AJP на 8009). Коннекторы принимают входящие TCP-соединения от клиентов, парсят данные протокола и формируют объекты Request/Response для дальнейшей обработки. Tomcat предоставляет несколько реализаций коннекторов (HTTP (NIO/NIO2), APR/native, AJP).
  • Engine – движок Catalina, основной контейнер обработки запросов. Engine получает все запросы от коннекторов, связанных с данным сервисом, и направляет их к нужному веб-приложению для обработки. На уровне Engine можно настраивать кластеризацию (атрибут jvmRoute для балансировки) и глобальные Valve (специальные серверные фильтры).
  • Host – виртуальный хост, сопоставленный определенному DNS-имену (домену). Например, Host с именем “www.example.com” будет обрабатывать запросы с таким Host Header. В одном Engine может быть несколько Host’ов (для поддержки мультидоменности на одном Tomcat). Каждый Host имеет свой каталог приложений (appBase, по умолчанию webapps). Обычно используется Host с именем “localhost” для всех приложений.
  • Context – контекст веб-приложения (один развернутый WAR или папка). Каждый <Context> представляет отдельное веб-приложение, привязанное к контекстному пути (например, /app), и содержит набор сервлетов, JSP, фильтров, ресурсов, а также свой класс ClassLoader. Контекст обеспечивает изоляцию приложений друг от друга (отдельные класслоадеры, пространства имен JNDI, сессии и пр.).

Также внутри Context контейнером более низкого уровня является Wrapper, оборачивающий конкретный Servlet и управляющий его жизненным циклом. Однако вручную Wrapper’ы редко настраиваются – они создаются самим Tomcat на основе объявленных сервлетов.

Взаимосвязи компонентов. Server содержит один или несколько Service. Каждый Service объединяет один Engine и набор Connectors. Коннекторы “слушают” сетевые порты и передают запросы в Engine. Engine, получив запрос, выбирает Host по имени хоста запроса, затем внутри Host – находит подходящий Context по префиксу URL (контекстному пути) и передает ему запрос. Контекст, в свою очередь, определяет, какой Servlet (Wrapper) должен обработать данный запрос (по URL pattern’у, описанному в web.xml или через аннотации). От сервлета ответ проходит обратно вверх по цепочке: через фильтры Контекста, Valve Host/Engine (если есть) и возвращается Connector’у, который отправляет ответ клиенту по сети.

Наглядно архитектура Tomcat выглядит так: один Server содержит Service, а в нем – один Engine плюс несколько Connector’ов (например, HTTP и AJP). Engine включает в себя несколько виртуальных Host’ов, каждый из которых обслуживает набор Context’ов (веб-приложений). Ниже приведена обобщенная схема контейнеров:

Server
- Service ("Catalina")
    - Connector (HTTP/1.1, порт 8080)
    - Connector (AJP/1.3, порт 8009)
    - Engine ("Catalina")
        - Host ("localhost")
           - Context (path="", приложение ROOT)
           - Context (path="/myapp", приложение MyApp)
        - Host ("example.com")
            - Context (path="", приложение для example.com)

Примечание: в embedded-режиме Spring Boot обычно используется один Host localhost и один Context с путем / для самого приложения.

Официальная документация Apache Tomcat подробно описывает эти компоненты:

  • Server – целый экземпляр контейнера,
  • Service – связка Connectors с Engine,
  • Engine – механизм обработки запросов, распределяющий их по Host’ам,
  • Host – виртуальный хост, идентифицируемый доменным именем,
  • Connector – компонент для обмена по протоколу (HTTP, HTTPS, AJP),
  • Context – веб-приложение (контекст сервлетов).

Такая модульность позволяет гибко настраивать Tomcat – от добавления коннекторов (например, настроить одновременно HTTP и HTTPS) до запуска нескольких виртуальных хостов и изоляции веб-приложений.

Важно отметить, что Tomcat строго следует спецификации Jakarta Servlet. Запросы проходят через стандартную цепочку фильтров и слушателей событий, что позволяет перехватывать или модифицировать их на разных этапах (например, Servlet Filter и внутренние Valve Tomcat могут обрабатывать запрос до попадания в Servlet). Это предоставляет возможности для расширения функциональности – от логирования доступа (AccessLogValve) до ограничений IP (RemoteAddrValve) и т.д..

Рис. 1: архитектура Tomcat

В целом, архитектура Tomcat спроектирована для эффективности и масштабируемости. Например, коннекторы поддерживают неблокирующий I/O (NIO/NIO2) – это позволяет одному потоку обслуживать многие keep-alive соединения. Также Tomcat умеет при наличии native-библиотеки APR переключаться на использующий OpenSSL и системные вызовы сокетов режим для повышения производительности HTTP(S). Однако принципы работы контейнеров остаются одинаковыми вне зависимости от протокола соединения.

Жизненный цикл HTTP-запроса

Рассмотрим подробно, как HTTP-запрос обрабатывается внутри Tomcat, от момента установления TCP-соединения до возврата ответа Servlet’ом. Ниже представлена последовательность шагов (см. диаграмму):


Рис. 2: процесс обработки запроса в Tomcat: от приема соединения до вызова Servlet.

  1. Получение соединения. Клиент (браузер) открывает TCP-соединение к TCP-порту, который слушает Connector Tomcat (например, 8080 для HTTP). В Tomcat 10 коннектор по умолчанию работает в режиме NIO и использует один или несколько Acceptor-потоков для приема входящих соединений. Acceptor-поток, получив новый сокет, регистрирует его и передает свободному worker-потоку из пула коннектора, после чего сразу возвращается к прослушиванию следующего соединения.
  2. Парсинг HTTP-запроса. Выделенный worker-поток (из пула, размер которого по умолчанию maxThreads=200) начинает обработку запроса. Коннектор (компонент Coyote) читает из сокета сырые данные HTTP, парсит стартовую строку, заголовки и тело запроса. На этой стадии формируются объекты HttpServletRequest и HttpServletResponse (реализации Catalina). Коннектор заполняет в них всю информацию о запросе: метод, URI, заголовки, параметры и т.д. Например, URL GET http://www.testwebapp.com/sample/ будет разобран, и из заголовка Host: www.testwebapp.com коннектор извлечет имя хоста.
  3. Передача в контейнер. Коннектор вызывает метод CoyoteAdapter (связующее звено между Coyote и Catalina) – этот адаптер связывает низкоуровневый запрос с контейнером сервлетов. Он передает запрос на обработку в компонент Engine (Catalina Engine) через вызов engine.invoke(request, response). Engine определяет, к какому виртуальному Host адресован запрос: он смотрит на поле Host (например, “www.testwebapp.com”) и ищет соответствующий Host в конфигурации. Найдя нужный Host, Engine передает управление ему (host.invoke(…)).
  4. Выбор веб-приложения (Context). Host получает запрос (уже зная, что Host совпал по домену) и анализирует URI (путь запроса) для выбора Context. Каждый Context привязан к определенному path – например, путь /sample может соответствовать веб-приложению SampleApp. Host сопоставляет начало URI запроса с контекстными путями своих приложений и выбирает самое длинное совпадение. Допустим, запрос http://www.testwebapp.com/sample/ – Host “www.testwebapp.com” найдет Context с path /sample. Далее Host вызывает метод context.invoke(request, response) для передачи запроса внутрь выбранного веб-приложения.
  5. Обработка в веб-приложении. На уровне Context происходит наиболее подробная обработка. Контекст может выполнять ряд действий: применять Servlet Filters (глобальные для приложения), управлять сессией (привязать jsessionid к существующей или новой сессии), а также определить, какой Servlet должен обработать запрос. Для выбора сервлета Context использует конфигурацию маппинга URL->Servlet (в web.xml или аннотации). В нашем примере запрошен путь /sample/ – предположим, в приложении SampleApp настроен Servlet с URL pattern / (обрабатывающий все запросы приложения). Тогда Context найдет соответствующий Wrapper сервлета (объект, управляющий экземпляром сервлета). Wrapper при необходимости загружает и инициализирует Servlet (вызывая его init() один раз при первом обращении) и затем передает ему управление через вызов servlet.service(request, response).
    Перед непосредственным вызовом сервлета, Tomcat пропускает запрос через цепочку фильтров (filterChain.doFilter()), если в приложении определены фильтры. Фильтры могут перехватывать запрос (например, реализуя аутентификацию, логгирование, сжатие и др.), а затем либо продолжать цепочку (вызывая chain.doFilter()), либо возвращать ответ сами. В конце цепочки будет вызван сервисный метод сервлета (doGet()/doPost() и т.д.), куда разработчик помещает бизнес-логику обработки.
  6. Формирование ответа. Servlet генерирует ответ, записывая данные в объект HttpServletResponse (например, устанавливает код статуса, заголовки, выводит HTML в поток ответа). По завершении service() управление возвращается в Tomcat: Wrapper отмечает, что обработка завершена, и начинает обратный возврат по цепочке: пост-обработка фильтров (метод Filter.destroy() вызывается при остановке приложения, а не после каждого запроса, поэтому здесь фильтры выполняют только логику после chain.doFilter, если она предусмотрена). Далее ответ идет обратно через Context вверх: Context возвращает управление хосту Host – Engine, а Engine – коннектору. Коннектор отправляет HTTP-ответ по открытому сокету клиенту и может удерживать соединение открытым для последующих запросов (если заголовок Connection: keep-alive). После отправки ответа worker-поток освобождается (возвращается в пул).

Таким образом, полный путь запроса проходит через все уровни контейнеров: Connector -> Engine -> Host -> Context -> Servlet/Filter (и обратно). Ни один этап не пропускается: если, к примеру, для запроса не нашлось подходящего Host или Context, Tomcat вернет ошибку (обычно 404 Not Found или 400 Bad Request) еще на соответствующем уровне. Например, если домен запроса не сконфигурирован, Engine не найдет Host – и вернет 400/Bad Request. Если контекст не найден – Host вернет 404/Not Found.

Следует отметить, что Tomcat предоставляет множество расширений, которые могут участвовать в этом жизненном цикле. К ним относятся внутренние Valve – они подобны фильтрам, но задаются на уровне контейнеров (Engine, Host, Context) в конфигурации Tomcat. Например, ErrorReportValve в Host’е отвечает за оформление страниц ошибок, AccessLogValve – за логирование запросов. Valve срабатывают при вызове pipeline.invoke() на соответствующем контейнере, оборачивая вызовы вложенных компонентов. Также существуют Listener’ы – объекты, реагирующие на события жизненного цикла (запуск/остановка компонентов). Tomcat регистрирует ряд таких слушателей (например, JasperListener для инициализации JSP-движка Jasper, GlobalResourcesLifecycleListener и др. – их можно увидеть в конфигурации server.xml).

В целом, sequence-диаграмма обработки запроса можно резюмировать так:

  • Connector (Acceptor + worker-пул) принимает соединение и создает HTTP-запрос
  • Engine определяет виртуальный хост
  • Host определяет веб-приложение
  • Context определяет Servlet (через фильтры)
  • Servlet формирует ответ, который возвращается клиенту тем же путем.

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

Встроенное использование в Spring Boot

Одно из преимуществ Spring Boot – автоматическая интеграция встроенного Tomcat. При создании Spring Boot приложения с зависимостью spring-boot-starter-web вы по умолчанию получаете embedded Tomcat (в Spring Boot 3.x это Tomcat 10.x, в Boot 2.x – Tomcat 9) запускаемый внутри вашего .jar приложения. Это упрощает деплой: приложение запускается самодостаточным процессом, не требуя внешнего контейнера.

Как Spring Boot конфигурирует Tomcat: при старте приложения Spring Boot создает экземпляр класса TomcatServletWebServerFactory. Этот фабричный компонент программно настраивает Tomcat: выбирает номер порта (свойство server.port, по умолчанию 8080), адрес бинда (server.address, по умолчанию 0.0.0.0 – слушать на всех интерфейсах), контекстный путь (server.servlet.context-path, по умолчанию “/” – корневое приложение), а также применяет прочие настройки по умолчанию. Затем фабрика запускает Tomcat (класс org.apache.catalina.startup.Tomcat) – фактически, инициализирует Server, Service, Connector и контекст Spring-приложения.

Spring Boot подключает веб-приложение к Tomcat следующим образом: он создает единственный Host (имя “localhost”) и регистрирует в нем Context, соответствующий вашему Spring Boot приложению. Все ваши контроллеры, сервлеты, ресты работают внутри этого контекста. Путь контекста можно изменить через server.servlet.context-path. Статический контент (ресурсы в classpath) мапится на стандартный DefaultServlet Tomcat’а. Таким образом, Boot-приложение по структуре идентично WAR, развернутому на внешнем Tomcat, но Tomcat запускается на лету внутри приложения.

Настройки по умолчанию Tomcat в Spring Boot рассчитаны на типичные случаи, но их можно и нужно переопределять при необходимости. Основной способ – задать свойства в application.properties (или YAML). Некоторые из важных свойств (Spring Boot 2.x/3.x):

  • server.port – порт HTTP (по умолчанию 8080).
  • server.address – сетевой интерфейс для бинда (0.0.0.0 по умолчанию, слушает на всех).
  • server.tomcat.max-threads – максимальное число worker-потоков в пуле коннектора Tomcat. По умолчанию 200, можно увеличить при высоконагруженных приложениях или снизить в малоресурсной среде. Например, server.tomcat.max-threads=300 (в Spring Boot 2.x свойство может называться server.tomcat.threads.max).
  • server.tomcat.min-spare-threads – минимальное число резервных потоков (по умолчанию 10) – обычно нет нужды трогать.
  • server.tomcat.accept-count – длина очереди TCP-соединений, ожидающих свободного потока (backlog, по умолчанию 100). Если все потоки заняты, новые соединения ставятся в эту очередь; сверх этого числа – отклоняются с Connection Refused. В Boot можно увеличить accept-count, чтобы временно буферизовать больше запросов, но слишком большое значение может съесть ресурсы.
  • server.tomcat.max-connections – максимальное число открытых соединений, которые коннектор будет обслуживать одновременно (по умолчанию 10000 для NIO). Это включает keep-alive подключения, даже если они в ожидании. При достижении лимита коннектор начнет отклонять новые соединения до освобождения слотов.
  • server.connection-timeout – таймаут ожидания данных запроса по открытому соединению. По умолчанию 20 секунд (20000 мс). Можно настроить, например, server.connection-timeout=5s для более быстрой очистки зависших подключений.
  • server.max-http-header-size – максимальный размер HTTP-заголовков, по умолчанию 8KB. При необходимости (например, если используются очень большие cookies или auth-токены) можно увеличить, напр. server.max-http-header-size=16KB.
  • server.tomcat.max-http-post-size – максимальный размер тела запроса (для POST). По умолчанию 2MB; -1 отключает лимит. Лучше ограничивать для предотвращения злоупотреблений.
  • server.tomcat.uri-encoding – кодировка URI, стандартно UTF-8.

Также доступны настройки HTTPS (SSL): server.ssl.* свойства для включения TLS на встроенном Tomcat (указать keystore, пароли, протоколы и пр.) – Boot позволяет задействовать HTTPS практически так же, как на внешнем Tomcat, прописав сертификаты в properties.

Spring Boot позволяет программно кастомизировать Tomcat, если конфигурационных свойств недостаточно. Например, можно реализовать интерфейс WebServerFactoryCustomizer<TomcatServletWebServerFactory> и в методе customize() настроить свойства Factory: добавить дополнительный коннектор (например, HTTP на 8080 и HTTPS на 8443 одновременно – это нельзя через properties, но можно программно), зарегистрировать дополнительные Valve или коннекторы. Boot упрощает эту задачу, предоставляя удобные точки расширения.

Несколько примеров часто требуемых настроек: 

  • Изменение размера пула потоков: через server.tomcat.max-threads мы уже обсудили. Если приложение I/O-интенсивное, но не нагружает CPU, стоит убедиться, что maxThreads достаточно велико, чтобы обслуживать параллельные запросы. Однако слишком большое значение тоже вредно – может съесть память и вызвать лишний контекст-свитчинг.
  • Очередь и соединения: accept-count и max-connections – баланс между отказом в обслуживании и перегрузкой сервера. В Boot можно задать server.tomcat.accept-count и server.tomcat.max-connections при нестандартных потребностях (например, long-polling приложение может держать много соединений, тогда max-connections стоит увеличить). 
  • Timeouts: server.connection-timeout – важен для освобождения зависших соединений. Если клиентов много и возможны “повисшие” запросы, таймаут надо ограничить (например, 5-10 секунд). 
  • Размеры пакетов: server.max-http-header-size – для больших JWT или cookie. server.tomcat.max-http-post-size – ограничение на upload. При необходимости, Boot позволяет эти параметры менять.

Spring Boot встроенный Tomcat по функциональности не урезан по сравнению с обычным Tomcat. Он также поддерживает JNDI ресурсы (DataSource и пр.), если настроить через TomcatServletWebServerFactory (например, добавить Context.Resource), и все средства мониторинга (JMX). Единственное – встроенный Tomcat запускается в том же процессе, поэтому, в отличие от standalone, нет отдельного каталога conf/, logs/ и пр. Логи по умолчанию пишутся в консоль (и файл через Spring Boot logging config), JMX включается при запуске JVM с -Dcom.sun.management.jmxremote и т.д.

Таким образом, Spring Boot берет на себя большую часть конфигурационной рутины Tomcat, позволяя разработчику сконцентрироваться на логике приложения. Однако понимание настроек Tomcat по-прежнему необходимо: в производственной среде зачастую требуется тонкая настройка параметров, и зная, как они влияют на производительность, вы можете оптимизировать работу сервера. Например, увеличить maxThreads и acceptCount на узле, который испытывает наплывы запросов, или уменьшить connectionTimeout чтобы быстрее освобождать ресурсы – все это делается через Spring Boot properties или кастомизаторы, но основывается на тех же принципах, что и настройка standalone Tomcat.

Ключевые задачи, решаемые Tomcat

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

1. Управление потоками и очередь запросов. Tomcat обслуживает HTTP-запросы с помощью пула рабочих потоков (Thread Pool). По умолчанию на коннектор устанавливается maxThreads=200 – это максимум одновременно работающих потоков, обрабатывающих запросы. При старте Tomcat создает несколько потоков (определяется minSpareThreads, дефолт 10) и затем увеличивает их количество по мере роста нагрузки, вплоть до maxThreads. Если все потоки заняты и приходит новый запрос, он ставится в ожидание. Тут задействуется параметр acceptCount: операционная система ограничивает длину очереди сокетов, которые не были еще приняты приложением. По умолчанию Tomcat разрешает ставить до 100 запросов в очереди TCP на коннектор. Если очередь переполнена, новые подключения будут отклоняться (клиент получит отказ соединения). Эти параметры предотвращают “захлебывание” сервера: лучше отказать лишнему клиенту, чем повесить сервер, исчерпав ресурсы. Администратор может настроить их под свои требования – например, при кратковременных пиках нагрузки можно увеличить acceptCount, либо при большом числе I/O bound запросов – увеличить maxThreads. Tomcat также позволяет вынести пул потоков на уровень Service – компонент Executor: несколько коннекторов могут делить единый пул потоков (общий Executor). Это удобно, если у вас, к примеру, 2 коннектора (HTTP и HTTPS) и хочется ограничить суммарное число потоков. Executor управляет общим пулом с параметрами maxThreads, minSpareThreads и очередью maxQueueSize. Если очередь экзекьютора переполняется, новые задачи отклоняются с исключением RejectedExecutionException. Таким образом, Tomcat решает проблему потокового лимитирования и очередей “из коробки”, предоставляя настройки, которые можно оптимизировать под требуемый уровень конкурентности и ресурсов.

2. Ограничение количества соединений (Connection pool). Речь идет о пуле сетевых соединений HTTP, а не JDBC. Tomcat (особенно в NIO/NIO2 режимах) способен держать тысячи keep-alive соединений. Параметр maxConnections ограничивает общее число открытых сокетов, которые коннектор обрабатывает одновременно. В NIO по умолчанию 10000, что достаточно для большинства случаев. Этот “коннекшн пул” не является заранее созданными соединениями, а просто лимитом – но он предотвращает ситуацию, когда сервер истощит дескрипторы файлов или память, удерживая слишком много открытых соединений. Если клиент установит соединение сверх maxConnections, оно будет поставлено в ожидание на уровне ОС (в backlog очереди) и Tomcat примет его только когда количество активных снизится. Связка maxConnections и acceptCount защищает сервер от перегрузки соединениями. Например, в атаке типа Slowloris много соединений удерживаются полузакрытыми – Tomcat, ограниченный maxConnections, не упадет, а новые соединения просто не будут приняты сверх лимита. Еще один параметр – connectionTimeout – решает проблему зависших соединений: по истечении таймаута Tomcat закрывает неактивное соединение. Это препятствует истощению потоков на “висящих” клиентах. Таким образом, Tomcat управляет пулом входящих соединений схоже с пулом потоков: держит активные в пределах нормы и вовремя очищает лишние.

3. Безопасность и контроль доступа. Tomcat включает ряд механизмов для обеспечения безопасности веб-приложений: 

  • TLS (SSL) поддержка: Tomcat может принимать HTTPS – достаточно сконфигурировать <Connector port=”8443″ protocol=”HTTP/1.1″ SSLEnabled=”true” …> с указанием keystore и сертификата. В Tomcat 10 встроена поддержка TLS 1.3 (при Java 11+). Есть возможности тонкой настройки: выбор протоколов, шифров, требование клиентских сертификатов и пр. (через атрибуты sslProtocol, ciphers, clientAuth и др.). Таким образом, Tomcat решает задачу шифрования трафика штатно, без внешнего прокси. Если используется Spring Boot, включение TLS – вопрос установки свойств server.ssl.* (keystore, пароли). 
  • Ограничение ресурсов и запросов: через конфигурацию можно задать лимиты на размер заголовков (maxHttpHeaderSize), URL, количество параметров (maxParameterCount), размер загружаемых файлов (Multipart config). Это защищает от злоупотреблений (слишком большие cookies, headers-бомбы и т.п.).
  • Безопасные настройки по умолчанию: Tomcat по дефолту достаточно безопасен для продакшена. Он не позволяет directory listing, имеет ограничение на запросы менеджера по IP, и т.д. В документации есть раздел Security Considerations, перечисляющий рекомендуемые настройки (например, отключить авто развертывание приложений на проде, запускать Tomcat от не суперпользователя и др.). Tomcat из коробки “разоружен” относительно потенциальных уязвимостей – например, не позволяет устраивать XSS через автоматическое определение кодировки (в conf/web.xml включен фильтр AddDefaultCharset). 
  • Контейнерные фильтры безопасности: Tomcat предоставляет ряд встроенных Filter-ов, которые можно глобально включить. Например, HTTP Header Security Filter добавляет заголовки безопасности (X-Content-Type-Options, XSS-Protection, HSTS и др.), CSRF Prevention Filter – предотвращает CSRF-атаки, CORS Filter – обрабатывает CORS-запросы. Их можно прописать в conf/web.xml, и они будут применяться ко всем приложениям на сервере. Это решает типичные задачи hardening’а без изменения кода приложений.
  • Встроенная аутентификация и авторизация (Realms): Tomcat умеет сам выполнять проверку пользователей, если вы используете декларативную безопасность (описанную в web.xml). Поддерживаются Realms – подключения к хранилищам учетных данных: MemoryRealm, DataSourceRealm, JNDI/LDAP Realm и др. Например, можно настроить JDBCRealm, чтобы Tomcat сверял логин/пароль по таблице БД. Для встроенного Tomcat (Spring Boot) это реже используется, но в Tomcat как сервис – весьма распространено. 
  • Менеджмент сессий: Tomcat решает проблему хранения HTTP-сессий. По умолчанию он хранит их в памяти (StandardManager), но поддерживается персистентность (например, через PersistentManager на диск) и распределенное хранение для кластера (DeltaManager или использование внешних решений). В Tomcat 10 есть поддержка сессий на базе Redis/Memcached (через дополнительные модули), что помогает масштабировать приложение без “липкости” сессий. Это в контексте безопасности – предотвращает потерю сессий при перезагрузке и позволяет применять HttpOnly/secure-флаги к cookie. 
  • Запуск под ограниченной учетной записью и sandbox: Рекомендуется запускать Tomcat от отдельного не суперпользователя, с минимальными правами (например, user tomcat), а файловую систему настроить на недоступность конфигов посторонним. Также Tomcat может работать под SecurityManager’ом Java, что ограничивает права веб-приложений (хотя в современном Jakarta EE это используется редко).

Tomcat конфигурируется безопасно по умолчанию, но опытный инженер может усилить безопасность: включить нужные фильтры, настроить строгие TLS-режимы, убрать неиспользуемые коннекторы, запретить небезопасные методы (например, через RemoteAddrValve закрыть админку от всех, кроме localhost).

4. Обработка асинхронных запросов. Начиная с Servlet 3.0, Tomcat поддерживает асинхронные сервлеты. Это позволяет не блокировать рабочий поток на время длительной операции. Как это решает проблемы? Например, у вас долгий опрос или поток данных: вместо того чтобы поток Tomcat ждал минуту, можно вызвать request.startAsync() и отпустить поток обратно в пул, а когда результат будет готов – завершить запрос асинхронно. Tomcat реализует AsyncContext согласно спецификации. При вызове startAsync, Tomcat освобождает текущий worker-поток, и дальнейшая обработка должна быть выполнена разработчиком в отдельном (не томкетовском) потоке. Когда готов ответ, вызывается asyncContext.complete(), и Tomcat уже в другом потоке отправляет ответ клиенту. Этот механизм позволяет одному Tomcat-потоку обслуживать другие запросы, пока первый запрос “ждет” результата. Тем самым значительно повышается масштабируемость при I/O-bound нагрузках. NIO-коннектор Tomcat особенно выигрывает от async: он может держать тысячи открытых запросов, не занимая поток на каждый. Это решает классическую проблему “длинных опросов” и чата: можно держать соединение открытым (Servlet 3.0 async или WebSocket) и не тратить драгоценные потоки впустую.

Tomcat 10 улучшил поддержку асинхронности: он полностью неблокирующий в обработке запросов, поддерживает Servlet 4.0 non-blocking I/O (чтение/запись в запрос/ответ без блокировки), а также WebSocket 2.0 (стандарт Jakarta WebSocket). То есть, вы можете легко использовать WebSocket для двунаправленной связи – Tomcat выступает веб-сокет сервером (в Boot это также прозрачно – достаточно аннотировать класс как @ServerEndpoint). Асинхронная обработка и веб сокеты помогают реализовывать высоконагруженные realtime-приложения.

Конечно, при асинхронной работе важно контролировать ресурсы: Tomcat предоставляет параметры таймаута асинхронного контекста (по умолчанию 30 секунд), после которого, если complete() не вызван, контейнер автоматически завершит запрос и освободит ресурсы. Это защищает от “зависания” асинхронных задач.

5. Управление ресурсами и предотвращение утечек. В длительно работающих приложениях важна защита от утечек памяти и дескрипторов. Tomcat внедряет несколько механизмов: 

  • Предотвращение распространенных утечек при undeploy/redeploy: Проблема Java-платформы – класслоадеры веб-приложений могут оставаться в памяти, если какие-то статические поля или потоки не освобождены. Tomcat с версии 6.0.24 внедрил проверку распространенных случаев утечек: например, если веб-приложение не deregister-нул JDBC-драйвер, Tomcat это обнаружит и форсированно регистрирует драйвер при undeploy, логгируя предупреждение. “SEVERE: … JDBC Driver has been forcibly unregistered to prevent a memory leak” – распространенное сообщение, информирующее, что Tomcat убрал за приложением утечку. Аналогично, Tomcat пытается предотвратить утечки через ThreadLocal: есть ThreadLocalLeakPreventionListener, который инициализирует некоторые классы (как DriverManager, Logging) заранее, чтобы они привязались к системному класслоадеру, а не к веб-приложению. Это снижает риск утечек при перезагрузке. 
  • Ограничение числа открытых файлов/сокетов: уже упомянутые maxConnections и timeouts работают и как защита от утечек – например, если приложение забыло закрыть Response OutputStream, Tomcat все равно закроет сокет по таймауту. Также опция trackLockedFiles (в конфигурации Resources) может помочь отладить утечки файловых дескрипторов: Tomcat будет отслеживать, где был открыт файл и кто не закрыл. 
  • JMX и инструменты мониторинга: Tomcat предоставляет MBean’ы для отслеживания метрик – в том числе и таких, как число загруженных классов, количество активных потоков, открытые сессии и пр. По JMX можно заметить, растет ли, например, currentThreadsBusy без спадов (может указывать на утечку потоков) или увеличивается ли постоянно память (подозрение на утечку). Интеграция с monitoring-системами (JavaMelody, Prometheus) позволяет рано обнаружить потенциальные проблемы. Например, JavaMelody прямо указывает “Possible memory leak detected” при redeploy, если находит подозрительные ссылки.

6. Поддержка масштабирования и отказоустойчивости. Tomcat решает и задачи горизонтального масштабирования: 

  • Кластеризация сессий: Tomcat имеет встроенный механизм репликации HTTP-сессий в кластере – DeltaManager и BackupManager. При включении <Cluster> в конфиге Tomcat, серверы в группе обмениваются сообщениями (через TCP или UDP, Tomcat Tribes) и копируют друг другу сессии при изменениях. Это сложный механизм, но он позволяет реализовать sticky-free балансировку (клиент может переключиться на другой узел без потери сессии). В Tomcat 10 кластеризация доступна, хотя в мире Spring Boot часто предпочитают выносить хранение сессий во внешний сторедж (Redis, etc.), но Tomcat тоже имеет решение “из коробки”. 
  • Балансировка нагрузки (в связке с внешним сервером): Хотя Tomcat сам по себе не балансирует, он предоставляет AJP Connector, через который связка Apache HTTPd + mod_jk/mod_proxy или другие балансировщики могут эффективно распределять запросы. Tomcat оптимизирован для работы за реверс-прокси: поддерживает заголовки X-Forwarded- (при включении RemoteIpValve) для корректной обработки схемы (http/https) и IP клиента. 
  • Долговременная стабильность: Многие используют Tomcat годами без перезапуска. Его механизмы управления памятью и ресурсами помогают избежать “раздувания”: ThreadPool может быть настроен с maxIdleTime (когда Executor используется) – неиспользуемые потоки убираются через время простоя. Static ресурс-кеш регулируется (cacheMaxSize) и очищается. Такие детали позволяют Tomcat работать под нагрузкой длительное время без деградации.

Подводя итог: Apache Tomcat адресует ключевые проблемы веб-сервера – ограничения по потокам и соединениям, безопасное выполнение кода, планирование асинхронных задач, масштабирование – средствами конфигурации и встроенных компонент. Для разработчика важно знать об этих механизмах, чтобы уметь их правильно настроить. Например, высоконагруженное Spring Boot приложение может потребовать тюнинга Tomcat: увеличить пул потоков, настроить keep-alive timeout, включить сжатие ответов (server.compression.enabled=true), ограничить размер очереди – все это непосредственно влияет на выдерживаемую нагрузку и устойчивость приложения.

Tomcat, будучи “строгим” к спецификации, также гарантирует предсказуемое поведение приложений. Многие проблемы (например, deadlock или resource leak) чаще вызваны логикой приложений, но Tomcat старается минимизировать негативные эффекты. В следующем разделе рассмотрим особые возможности Tomcat 10 и, в частности, виртуальные потоки JDK (Project Loom), которые призваны радикально улучшить масштабируемость потоковой модели.

Особенности Tomcat 10 (Jakarta EE 9/10)

Версия Apache Tomcat 10 принесла ряд изменений по сравнению с предыдущими ветками (8.5/9.0), при этом архитектурно сервер остался тем же. Главные особенности Tomcat 10:

Переход на Jakarta EE и смена пространства имен. Начиная с Tomcat 10.0, сервер реализует спецификации Jakarta EE 9/10, что означает переход от пакетов javax.* к jakarta.*. Например, Tomcat 9 поддерживал Servlet 4.0 API (пакет javax.servlet.*), а Tomcat 10 поддерживает Servlet 5.0/6.0 (пакет jakarta.servlet.*). Это несовместимое изменение: веб-приложения, использующие старые пакеты (javax) нужно скомпилировать против новых Jakarta API. По сути, весь API сервлетов, JSP, EL был переименован. Инструментально это часто решается с помощью Maven-плагина Jakarta EE Migration, который автоматически переименовывает импорты в байткоде или исходниках. С точки зрения кода Tomcat, внутренних изменений было мало – это тот же код, но пакеты другие. Apache позиционирует Tomcat 10 как в основном аналог Tomcat 9, полностью совместимый по функционалу, кроме namespace’ов. То есть, если вы мигрировали свое приложение на jakarta.servlet API, оно должно работать на Tomcat 10 так же, как раньше на 9. Основное внимание при переходе – именно обновление зависимостей (JSP тег-либы тоже сменили URI, etc.). Это глобальное изменение в экосистеме Java EE: Tomcat 10 был одним из первых серверов, перешедших на Jakarta.

Поддержка новейших спецификаций. Tomcat 10.0 соответствует Jakarta EE 9 (Servlet 5.0, JSP 3.0, EL 4.0, WebSocket 2.0, Authentication 2.0). Последующие Tomcat 10.1 соответствует Jakarta EE 10 (Servlet 6.0, JSP 3.1, EL 5.0, WebSocket 2.1 и пр.). Новые версии спецификаций принесли некоторые улучшения: 

  • Servlet 6.0 (Jakarta EE 10) – скорее минорные изменения, но заметно улучшена интеграция с HTTP/2. Servlet 6 требует поддержки HTTP/2 из коробки. Tomcat 10.0 уже умел HTTP/2 (при Java 9+ или через libtcnative на Java 8), а Tomcat 10.1 на Java 11+ имеет HTTP/2 by default. HTTP/3 пока не поддерживается напрямую (ни в Tomcat 10, ни даже в 10.1) – при необходимости HTTP/3 (QUIC) обычно ставят реверс-прокси (nginx, Caddy) перед Tomcat.
  •  JSP 3.1 – обновление JSP, входящее в Jakarta EE 10. Tomcat 10.1 включает обновленный JSP-движок Jasper 3.0. Одно из важных улучшений – оптимизация производительности компиляции JSP. В релизнотах Tomcat 10.1 отмечено, что Jasper был переработан для ускорения компиляции JSP и уменьшения нагрузки. JSP с использованием современных функций (например, поддержка новых спецификаций Expression Language 5.0) также обеспечивается. 
  • WebSocket 2.1 – Tomcat 10.1 обновил поддержку JSR 356 WebSocket API до версии 2.1. Из улучшений – поддержка передачи объектов (новый маппинг сообщений) и улучшенная обработка пингов. Для разработчика это мало заметно: Tomcat как поддерживал WebSocket (вплоть до протокола RFC6455), так и поддерживает, но небольшие обновления API учтены. 
  • Jakarta Authentication 3.0 – Tomcat 10.1 включает поддержку Jakarta Authentication (ранее JASPIC) 3.0, но в standalone Tomcat это не часто используется.

Производительность и оптимизации. Хотя Tomcat 10 не рекламировался как заметно более быстрый, под капотом были сделаны рефакторинги и улучшения

  • В Tomcat 10.0/10.1 уделено внимание ускорению запуска приложений. Например, введена опциональная ExtractingRoot для ресурсов: Tomcat может при старте распаковывать JAR’ы приложения во временной каталог, чтобы ускорить дальнейший доступ и сканирование. Это особенно помогает, если в WAR-е очень много JAR-зависимостей – вместо чтения их из ZIP-архива Tomcat распакует их на диск, и сканирование annotation/digest происходит быстрее. 
  • Улучшены структуры данных для метаданных. В упоминаниях разработчиков фигурирует переход на более эффективные коллекции при парсинге web.xml и annotation scanning. Это снижает накладные расходы при разворачивании приложения. 
  • NIO vs NIO2: хотя Tomcat 9 уже имел NIO2-коннектор, в Tomcat 10 разработчики проанализировали производительность и пришли к выводу, что NIO2 не дает существенного выигрыша над NIO (в контексте Java улучшений). Один из коммиттеров Tomcat отметил, что NIO в JDK сильно оптимизировали, поэтому NIO2-коннектор не стал быстрее NIO. Однако Tomcat 10 продолжает поддерживать оба, так что администратор может выбрать. По умолчанию (в 10.0/10.1) используется Http11NioProtocol. 
  • Исправления и патчи безопасности: Tomcat 10 получил все накопленные фиксы Tomcat 9. Последние версии 9.x фокусировались на security-патчах, тогда как новые фичи идут в 10.x. То есть, Tomcat 10 на момент выхода уже включал устранение уязвимостей, выявленных в 9.x (например, GhostCat CVE-2020-1938, если помните, был фикснут еще в 9.0.31 и, конечно, 10 не был уязвим).

В целом, сравнение Tomcat 9 и Tomcat 10 можно подытожить так: новая нумерация – старый добрый Tomcat, но на Jakarta API. Разработчикам важно знать, что миграция на Tomcat 10 требует миграции кода (простого переименования импортов и зависимостей с javax на jakarta). Функционально отличия минимальны: производительность и стабильность сопоставимы. Как сказано в одном обзоре, “между Tomcat 9 и 10 нет больших фич-различий, кроме смены пакетов и security/maintenance обновлений”.

Некоторые новые возможности Java становятся доступны с Tomcat 10.1, требующим Java 11. Например, полная поддержка TLS 1.3 (которая в Java 8 отсутствовала), более удобная настройка протоколов HTTP/2 без внешних либ, да и Project Loom (виртуальные потоки) – о них далее – можно реально использовать только начиная с JDK 19+ (виртуальные потоки), а Tomcat 9 официально на Java 19 тоже запускается, но из коробки Loom-поддержка появилась в ветке 10.1/11.

Также отметим, что Tomcat 10.x, начиная с 10.1, планирует более короткий жизненный цикл поддержки. Ожидается Tomcat 11 (под Jakarta EE 11) в 2023-2024 годах, после чего Tomcat 10.0 уже переведен в режим end-of-life, а 10.1 будет поддерживаться некоторое время. Поэтому, если вы начинаете новый проект на Spring Boot 3 (который тянет Tomcat 10), знайте, что обновления безопасности будут выходить и требовать обновления мажорной версии контейнера.

Вывод: Tomcat 10 – это эволюция, а не революция. Он обеспечивает совместимость с современным стеком Jakarta EE, включил небольшие оптимизации (быстрый старт, Servlet 6.0), но в остальном ведет себя так же, как Tomcat 9. Разработчику важно уделить внимание именно переезду на новые namespaces – все web.xml, собственный код фильтров/слушателей должны использовать jakarta.* классы. Многие библиотеки (Spring, Hibernate и др.) выпустили Jakarta-совместимые версии; на уровне Spring Boot переход с Boot 2 (Tomcat 9, javax) на Boot 3 (Tomcat 10, jakarta) – значимое изменение. Но после успешной миграции можно ожидать такого же надежного поведения. При этом Tomcat 10 уже “на будущем пути”: например, Spring Framework 6 требует Jakarta, так что Tomcat 10 – естественный выбор.

Наконец, Tomcat 10.1 потребовал Java 11 (Tomcat 10.0 работал и на Java 8). Это открывает дорогу интеграции с новыми возможностями JVM, например Loom. Рассмотрим же Project Loom и виртуальные потоки в контексте Tomcat 10.

Виртуальные потоки (Project Loom) и Tomcat 10

Одним из самых значительных новшеств в Java мире последних лет стал Project Loom – появление виртуальных потоков (virtual threads) в Java 19+ (стабильно в Java 21). Виртуальные потоки – это легковесные потоки, реализованные в пользовательском пространстве, которые существенно дешевле системных (OS) потоков. Они позволяют создавать десятки и сотни тысяч конкурентных задач без перегрузки планировщика ОС. В контексте серверов, таких как Tomcat, Loom обещает кардинально улучшить масштабируемость, устранив ограничение “один запрос – один OS-поток”.

Tomcat и Loom: начиная с ветки Tomcat 10.1, была экспериментально добавлена поддержка виртуальных потоков. Это выражается в нескольких моментах: 

  • В Tomcat 10.1 коннектор NIO (Http11NioProtocol) умеет запускать обработку запросов на виртуальных потоках, если указать настройку useVirtualThreads=”true” на Connector. При этом вместо обычного пула ThreadPoolExecutor Tomcat будет использовать специальный VirtualThreadExecutor (на основе Executors.newVirtualThreadPerTaskExecutor() из JDK). 
  • Виртуальные потоки доступны только на Java 19+ (в 19/20 – preview, в 21 – GA). Tomcat 10.0 (работавший на Java 8) про Loom ничего не знал. Tomcat 10.1 (Java 11+) частично поддерживает, а полноценную поддержку заявляет Tomcat 11. 
  • В конфигурации server.xml можно либо глобально задействовать VirtualThreadExecutor для всех коннекторов, либо на уровне каждого Connector прописать атрибут useVirtualThreads=”true”. Spring Boot 3.1+ тоже умеет включать Loom: если запустить на JDK 21 и задать property server.tomcat.virtual-threads=true (именно такое свойство, по обсуждениям Spring).

Как это работает внутри: при включенных virtual threads, Tomcat вместо традиционного пула с ограниченным количеством OS-потоков создает для каждого входящего запроса отдельный виртуальный поток и выполняет в нем обработку (Adapter -> Engine -> … -> Servlet). Виртуальные потоки очень дешевые: их можно иметь десятки тысяч, поэтому больше нет жесткой привязки maxThreads. По сути, maxThreads перестает ограничивать параллелизм (в Tomcat 10.1 при Loom все равно есть внутренний maxVirtualThreads – но он может быть очень большим, по умолчанию равен 200, надо проверить исходники). Главная выгода – отсутствие блокировок на ожидании I/O: если servlet “заснул” ожидая внешний ресурс (например, БД или HTTP вызов), виртуальный поток паркуется, а OS-поток, выполнявший его, освобождается для других задач. Это устраняет проблему, когда все OS-потоки заняты ожиданием, и новых обработчиков нет.

Практические ограничения и опыт (Netflix case): Хотя идея звучит великолепно, на практике Loom-интеграция встретила подводные камни. Компания Netflix, протестировав Java 21 + Spring Boot 3 (Tomcat embedded) с виртуальными потоками, обнаружила нестабильность и зависания в ряде сервисов. Расследование показало, что возникала своеобразная блокировка: Tomcat при Loom стал создавать виртуальный поток на каждый запрос, но во внутренней реализации Tomcat унаследовались некоторые синхронизации из старой модели, что приводило к пиннингу виртуальных потоков к OS-потокам. В частности, выявили synchronized-блок в NioEndpoint, из-за которого все виртуальные потоки могли ждать, держа OS-threads, и наступал дедлок-подобный сценарий. Проще говоря, “виртуальные потоки не избавляют от проблем блокировок в коде”. Если общий пул OS-потоков (ForkJoinPool, на котором по умолчанию работают virtual threads) исчерпан, а все virtual threads стоят на lock – новые virtual threads не могут быть запущены, и приложение висит.

Конкретно в Tomcat 10.1 тогда был баг: для NIO2-коннектора useVirtualThreads не работал вообще, а NIO-коннектор работал, но, как выяснилось, не полностью без блокировок. Netflix обнаружила тысячи “зависших” virtual threads, ожидающих освобождения монитора, при том что ни один не владел монитором (классический deadlock). Это привело к тем самым тайм-аутам и “неответам”. Они зафиксировали проблему и воспроизвели ее в тестовом примере. Apache уже внесла правки: проблема с NIO2 была зарегистрирована (Bug 68312), ее исправили в Tomcat 10.1.17+. Однако основной вывод – виртуальные потоки еще требуют тщательной обкатки и тюнинга в Tomcat.

Помимо багов, есть и другие ограничения: 

  • Блокирующий код в приложении: виртуальные потоки помогают только если код хорошо написан. Если в вашем приложении есть масштабируемые точки (например, JDBC-вызовы), то виртуальный поток при блокировке JDBC (которая на уровне драйвера все равно блокирует OS-поток) – не волшебная палочка. Выигрыш будет, если используются асинхронные драйверы или внешние сервисы, способные не блокировать поток (но JDBC традиционно блокирующий). Поэтому для типичного Spring Boot с JPA Loom не дает 10x ускорения. Бенчмарки показывают, что в I/O-bound задачах virtual threads выигрывают в throughput, но не кардинально, если упираются в те же ресурсы БД. 
  • ThreadLocals и прочее: Virtual threads ведут себя по отношению к ThreadLocal переменным – копируют значения из “несущего” (carrier) потока при старте. Это может привести к увеличению памяти, если есть много ThreadLocal. В Tomcat активно используются ThreadLocal (например, для DateFormat или BufferCache). Loom-разработчики постарались это учесть, но все нюансы проявляются со временем. 
  • Отладка и профилирование: виртуальные потоки – новый инструмент, требующий новых подходов к мониторингу. JMX MBeans Tomcat’а, например, currentThreadsBusy – как его трактовать при Loom? Он может всегда показывать 0 busy OS threads, хотя 1000 virtual threads выполняются. В самом Tomcat 10.1 MBean ThreadPool в контексте Loom утратил прежний смысл (Catalina:type=ThreadPool отображает не всю картину). Есть дискуссии о добавлении метрик виртуальных потоков в JMX (см. почтовую рассылку Tomcat) – возможно, это появится в Tomcat 11. 
  • Совместимость: Spring Framework 6+ уже совместим с Loom (Spring MVC может обрабатывать запросы на virtual threads, Spring WebClient может использовать Loom вместо Reactor). Однако библиотеки, не ожидающие сотни concurrent threads, теоретически могут иметь проблемы (например, ограничение числа descriptors и т.д.). В Tomcat таких ограничений нет – но и инфраструктура (библиотеки БД, драйверы) должна догнать.

Тем не менее, виртуальные потоки уже демонстрируют преимущество: в тестах Spring команда отметила повышение throughput на сервлетах при server.tomcat.virtual-threads=true примерно на 10-20% под высокой нагрузкой, и сильно сниженное потребление памяти на трейсы потоков. А главное – при использовании Loom можно отказаться от сложностей реактивного программирования ради масштабируемости. Код с обычными блокирующими операциями будет масштабироваться почти как реактивный, но с сохранением императивного стиля – это огромный плюс для разработчиков.

Практический совет: если вы хотите экспериментально включить Loom в Tomcat 10: 

  • Убедитесь, что используете последнюю версию Tomcat 10.1.x (в которой исправлены известные баги Loom). 
  • Запустите под JDK 21+ и укажите -Djdk.virtualThreadScheduler.maxPoolSize=<N> при необходимости настроить pool carrier-потоков (по умолчанию = кол-во процессоров).
  • В server.xml добавьте useVirtualThreads=”true” для Connector. В Spring Boot 3.1+ вместо этого можно в application.properties: spring.threads.virtual.enabled=true (или аналогичное свойство, актуальное на момент чтения). 
  • Тестируйте тщательно под нагрузкой! Обращайте внимание на метрики: если заметите Connection Refused ошибки – возможно, уткнулись в maxConnections/acceptCount ибо виртуальные потоки позволили принять очень много соединений, убедитесь, что и эти лимиты повышены если нужно.
  • Следите за логами Tomcat: при включении Loom Tomcat логгирует, что использует VirtualThreadExecutor. Также смотрите на потребление CPU: Loom может выжать больше CPU utilization, так что следите за GC, он может стать фактором (больше активных tasks -> больше аллокаций одновременно). 
  • Готовьтесь отключить Loom, если заметите нестабильность. В конечном счете, Tomcat 11 обещает полноценную поддержку с учетом всех багфиксов. Пока же Loom – для тех, кто экспериментирует.

Подытоживая: виртуальные потоки – перспективная технология, способная значительно повысить конкурентность сервлет-приложений. Tomcat 10 получил раннюю (частичную) поддержку Loom, и по мере взросления JDK, Tomcat наверняка станет одним из бенефициаров. Уже сейчас тесты показывают, что под большим числом параллельных запросов latency уменьшается и throughput растет при переходе на virtual threads (особенно, если приложение проводит много времени в ожидании I/O). Но также выявлено, что неправильный код или неожиданные блокировки могут нейтрализовать преимущество Loom или даже привести к дедлоку. Рекомендация – начинать знакомиться с Loom, писать приложения без синхронных блокировок (в идеале), тогда в будущем вы сможете легко переключиться на виртуальные потоки, и Tomcat (или любой совместимый контейнер) автоматически даст масштабируемость без переписывания на WebFlux/Reactor.

В контексте Spring Boot пока Loom – опция “для энтузиастов”, но высока вероятность, что через 1-2 года она станет мейнстримом. Apache Tomcat как проект активно двигается в эту сторону, и, будучи легким контейнером, прекрасно подходит для демонстрации возможностей Loom (в отличие от, скажем, тяжеловесных Java EE серверов). Так что следим за обновлениями Tomcat 10.1.x и 11.

Наблюдаемость и метрики Tomcat

Для промышленной эксплуатации Tomcat мониторинг и наблюдаемость (observability) играют критически важную роль. Java-приложения порой ведут себя непредсказуемо под нагрузкой, поэтому надо собирать метрики и логи, чтобы своевременно обнаруживать проблемы: рост времени ответа, исчерпание потоков, очередей, утечки памяти и пр.

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

Ключевые метрики производительности: 

  • Количество запросов и скорость обработки – характеризуют нагрузку и throughput. Tomcat экспонирует MBean типа GlobalRequestProcessor (Domain: Catalina) для каждого Connector, где есть атрибуты requestCount (число обработанных запросов с момента старта) и processingTime (суммарное время обработки всех запросов). На основе них можно рассчитать requests per second и average response time. Также там есть errorCount, bytesSent, bytesReceived и maxTime (максимальное время обработки одного запроса). Эти метрики полезны: например, requestCount растет линейно – можно брать его разницу в единицу времени (МBean-ы cumulative), либо если используете HTTP Access Log – считать по логам. Мониторинг RPS (запросов в секунду) позволяет заметить падение трафика (может сервис недоступен) или резкий всплеск (возможно, атака). 
  • Время отклика (latency) – в MBean видно maxTime (максимум), но лучше настроить на уровне APM или логов. Например, Tomcat Access Log можно включить (server.tomcat.accesslog.enabled=true в Boot) и туда добавить %D – время обработки запроса в миллисекундах. Тогда можно парсить логи или подключить их в мониторинг (например, Datadog, Splunk). Высокий processing time сигнализирует о проблемах: либо не хватает потоков (запросы ждут очередь), либо внешние зависимости (БД, API) тормозят.
  • Пул потоков – очень важно следить, не закончатся ли worker-потоки. MBean ThreadPool (Catalina:type=ThreadPool,name=”<connector>”) имеет атрибуты currentThreadCount, currentThreadsBusy, maxThreads. Например, для коннектора “http-nio-8080” можно через JMX увидеть: всего потоков 200, из них занято 50. Если currentThreadsBusy == maxThreads постоянно – это сигнал перегрузки: все потоки заняты, новые запросы ставятся в очередь (смотрите acceptCount). В таком случае увеличивайте maxThreads или добавляйте узлы, или оптимизируйте код. Многие инструменты (JavaMelody, JConsole) умеют показывать эти числа на графике.
  • Очередь соединений – Tomcat прямо не экспонирует длину текущей очереди acceptCount. Но можно косвенно: MBean GlobalRequestProcessor имеет requestCount и errorCount (отклоненные запросы увеличат errorCount? хотя “Connection refused” скорее не дойдет до Tomcat, а произойдет на OS). Лучше смотреть метрику на уровне ОС: SYN backlog. Однако можно заметить, что если currentThreadsBusy постоянно на максимуме, а requestCount растет медленнее чем поступающие запросы – есть очередь. Spring Boot Actuator ~выпускает metric tomcat.threads.busy и tomcat.threads.current, их можно в Micrometer/Prometheus отдать. 
  • Число активных соединений – MBean ThreadPool (Catalina) для NIO/NIO2 коннекторов также включает connectionCount (в Tomcat 10 – currentConnections?) – общее число открытых сокетов. В Tomcat 9/10 есть отдельный тип MBean: Connector (Catalina:type=Connector) с атрибутом maxConnections и возможно connectionCount. На практике мониторят количество ESTABLISHED TCP-соединений к порту – это можно получить через netstat/ss или OS metrics (например, proc/net/tcp). Если их слишком много, это может быть признаком не разорванных подключений (или атаки). 
  • Память JVM – Tomcat сам отдается JMX-метрики java.lang:type=Memory – heap usage, non-heap usage. Также JavaMelody, VisualVM и др. показывают графики. HeapMemoryUsage (used/committed/max) нужно отслеживать, чтобы убедиться, что нет memory leak или недостатка памяти. Если used растет и не снижается после GC, а приближается к max – вероятна утечка (например, сессии не очищаются). Tomcat по умолчанию не логирует OutOfMemoryError сильно, поэтому мониторинг памяти – обязанность вашей инфраструктуры. 
  • Размер пулов БД – если используете Tomcat JDBC Connection Pool (или HikariCP), тоже важно смотреть: занято ли все connections, нет ли connection leak (взят и не возвращен). Tomcat, если DataSource объявлен как JNDI Resource, экспонирует MBean DataSource с атрибутами numActive, numIdle. Это можно подключить в мониторинг. Если numActive регулярно равен maxActive – значит БД-пул исчерпан, запросы ждут коннекта => задержки. Либо увеличить пул, либо оптимизировать запросы.

Средства наблюдаемости: 

  • JMX (Java Management Extensions): Tomcat регистрирует MBean’ы в MBean-сервере JVM. Их можно просматривать через JConsole, VisualVM (плагин MBeans), или собирать программно/через агенты. Большинство метрик, перечисленных выше, доступны как JMX-атрибуты. В продакшене обычно не открывают JMX по сети (безопасность), но можно подключать JMX-экспортер (например, Prometheus JMX Exporter) или использовать Spring Boot Actuator. 
  • Spring Boot Actuator: если вы запускаете Tomcat внутри Spring Boot, то Actuator автоматически собирает некоторые Tomcat метрики через Micrometer. Например, метрики с префиксом tomcat.threads.* (пул потоков), tomcat.sessions.* (количество сессий, их создания/удаления), tomcat.global.sent, tomcat.global.request.max и т.д. – их можно увидеть на /actuator/metrics или настроить экспорт в Prometheus. Правда, чтобы Tomcat MBeans зарегистрировались, нужно включить server.tomcat.mbeanregistry.enabled=true в application.properties (по умолчанию Boot не регистрирует MBeans Tomcat’а). Actuator значительно упрощает получение метрик: например, tomcat.threads.config.max (максимум потоков) и tomcat.threads.busy (занятые) доступны сразу, или tomcat.sessions.active – число активных сессий. В продакшне, связав Actuator с Prometheus/Grafana, можно построить дешборды доступности. 
  • Логирование доступа (Access Log): это простой но полезный способ наблюдать за системой. Включив access-log Valve, можно в реальном времени видеть поток запросов, их статус, время обработки. Это позволяет не только аудит вести, но и быстро заметить, например, что какие-то запросы стали возвращать 5xx ошибки или обрабатываются очень долго (в логе можно вывести %T – время обработки секундах, или %D – в мс). Многие проблемы (таймауты, перегрузка) сначала отражаются на status code и response time – по логам это можно поймать раньше, чем пользователи начнут жаловаться. 
  • Потоковые дампы (Thread dumps): Не совсем метрика, но важный инструмент. Если подозревается deadlock или нехватка потоков, снимите jstack – посмотрите, чем заняты Tomcat-потоки. Бывает, что deadlock происходит из-за неправильного кода (например, синхронизация на сессии). Tomcat сам по себе избегает deadlock’ов, но, как видели, с Loom может возникнуть или, например, с некорректной Servlet (ждущей внутри себя). Дамп потоков покажет, если все 200 threads стоят в ожидании БД или заблокированы на монитор – станет ясно узкое место. 
  • Интеграция с APM/Monitoring системами: Существуют готовые интеграции – например, Datadog агент умеет напрямую снимать метрики Tomcat (у них есть default dashboard с ключевыми метриками: throughput, threads, error %, JVM memory). New Relic, AppDynamics и др. также подхватывают Tomcat-метрики и рисуют красивые графики. Если бюджет позволяет – это отличное решение, дающее и метрики, и профилирование запросов. 
  • JavaMelody: Опенсорсный lib, который можно подключить в веб-приложение. Он поставляется как фильтр, и при включении предоставляет веб-интерфейс мониторинга (URL типа /monitoring). JavaMelody собирает много метрик: нагрузка, время отклика, использование памяти, пула соединений, частота GC, и даже SQL-запросы. Удобно, что все в одной странице, и малый оверхед. На продакшене можно использовать с защитой (логин/пароль). Он, например, визуализирует те же MBeans (ThreadPool, GC). Многие компании пользуются JavaMelody как легковесным APM.

Какие параметры наиболее важны для наблюдения:
1) Доступность потоков – чтобы Threads Busy не равнялся постоянно Threads Max. Это индикатор, что сервер на пределе. Если видите 100% занятость, надо реагировать: масштабировать или оптимизировать.
2) Время ответа – SLA. Например, 95-й перцентиль времени ответа: если растет, значит начинаются проблемы (первые симптомы перегрузки либо проблем с внешними ресурсами).
3) Число ошибок (5xx) – если появляются внутренние ошибки или frequent timeouts (в логах 504, 408), то что-то не так: либо код, либо БД недоступна и т.д.
4) Потребление памяти – избежать OutOfMemory. Следите за Old Gen usage после Full GC: должен оставаться запас. Если утечки – по тренду заметите рост.
5) Количество сессий – если у вас stateful приложение, мониторьте Active Sessions. Резкий скачок может указывать на непредвиденную нагрузку (либо на то, что сессии не истекают как должны – возможна утечка сессий). Tomcat Actuator metrics дает tomcat.sessions.active, tomcat.sessions.created, tomcat.sessions.expired. 6) Использование CPU – по OS метрикам или Process CPU load JMX. Если CPU близок к 100% долгое время, сервер на пределе вычислительных возможностей: либо optimize, либо scale-out. 7) Очереди JMS/потоков – если Tomcat выполняет фоновые задачи (например, через ScheduledExecutor), тоже полезно мониторить, но это уже специфично для приложения.

В случае Spring Boot Actuator + Prometheus типичный набор метрик: tomcat_global_request_max_seconds (максимальное время запроса), tomcat_threads_busy и tomcat_threads_current, tomcat_sessions_active_current и tomcat_sessions_created_total, process_cpu_usage, jvm_memory_used_bytes по областям. На основе них строятся алерты: например, “Tomcat busy threads > 90% длительностью 5 мин” – предупреждение, “HTTP 5xx rate > X%” – проблема, “Средний latency > Y” – деградация.

В Standalone Tomcat (не Boot) можно также задействовать Manager App – у Tomcat есть веб-приложение Manager, где есть HTML-страница с статусом: сколько сессий, сколько запросов обработано, и даже кнопка “Find Leaks” (ищет утечки класслоадеров). Это немного устарело, но как экспресс-проверка сгодится. Туда же можно подключить JMXProxy – текстовый вывод MBean атрибутов через HTTP, если не можете иным образом их получить.

Итак, Tomcat весьма прозрачен для мониторинга: практически все, что нам нужно, можно достать через JMX. Главное – настроить сбор этих метрик на регулярной основе и установить разумные пороги оповещений. Особенно критично на проде не пропустить ситуацию, когда все потоки заняты и запросы начинают отваливаться – такие вещи должны сразу поднимать тревогу.

Также monitoring полезен для тюнинга: скажем, увидев, что peak load у вас использует максимум 50 потоков, можно снизить maxThreads до 100 вместо 200 – меньше потребление памяти и переключений. Или если заметно, что maxConnections в пике достигается (например, в Prometheus можно косвенно понять, если connect errors растут) – то увеличить maxConnections или добавить прокси с KeepAliveTimeout. Все решения по оптимизации должны опираться на наблюдаемые данные, а Tomcat дает все возможности их собрать.

Сравнение с другими серверами: Jetty, Undertow, Netty

Java-разработчикам доступно несколько альтернатив Tomcat для встраиваемых и standalone веб-серверов. Рассмотрим кратко, в чем Tomcat выигрывает, а где может уступать таким серверным контейнерам, как Eclipse Jetty, JBoss Undertow и фреймворку Netty (который хотя и не Servlet-контейнер, но используется в реактивных приложениях).

Apache Tomcat

  • Преимущества: самое широкое распространение и зрелость. За ~20+ лет Tomcat отточен и “закален в боях” – множество багов устранено, поведение предсказуемо. Он строго следует спецификации сервлетов и JSP, что упрощает переносимость приложений. Большинство стеков (Spring MVC, JSF и т.д.) отлаживались в основном на Tomcat – поэтому меньше сюрпризов. Также Tomcat богаче по функционалу управления: у него есть готовые админ-инструменты, обширная документация, обилие примеров в сообществе. Встраивание Tomcat в Spring Boot идет из коробки, с автоконфигурацией – на других иногда нужны дополнительные зависимости. 
  • Недостатки: исторически Tomcat считался немного тяжелее по памяти, чем Jetty или Undertow, хотя разница небольшая (десятки МБ). Его архитектура (поток на запрос, blocking I/O) долго была минусом для очень высокой конкурентности – но с появлением Loom это нивелируется. Также Tomcat не претендует на рекорды производительности: тесты показывают, что Undertow иногда обгоняет Tomcat по RPS на нескольких процентах, а Netty (будучи полностью асинхронным) может показать лучше latency под экстремальной нагрузкой. Но разница не радикальна: в среднем Tomcat, Jetty, Undertow дают сопоставимый throughput – все они способны обработать тысячи запросов/с (в одном процессе). 
  • Когда выбирать: Tomcat хорош “по умолчанию” – если нет особых причин менять, разумно использовать Tomcat. Он безопасный выбор для Spring приложений (действительно, большинство Spring Boot app работают на Tomcat). Также Tomcat необходим, если вам нужны JSP/EL (например, JSP рендеринг – Undertow по умолчанию не имеет JSP-движка встроенного, Jetty имеет свой аналог JSP).

Eclipse Jetty

  • Преимущества: Jetty – легковесный Servlet-контейнер, популярный в том числе для embedded usage. Он часто использовался в интернет-проектах (LinkedIn, Yahoo) из-за своей модульности и чуть более низкого footprint. Jetty славится быстрым стартом – запуск порой быстрее Tomcat на схожих приложениях. По памяти Jetty обычно требует немного меньше overhead (нет Jasper, механизм classloading полегче). Jetty очень гибок в конфигурации – его можно запускать не только как сервер, но и как библиотеку (например, in-process HTTP server для тестов). Также Jetty раньше других внедряет новшества: так, HTTP/3/QUIC – Jetty 12 уже имеет экспериментальную поддержку HTTP/3 (через консорциум-расширение), тогда как Tomcat нет. 
  • Недостатки: Сообщество Jetty меньше, документация менее подробная (хотя достаточная). В Spring Boot переключение на Jetty делается легко, но некоторые свойства конфигурации могут отличаться или не поддерживаться (Boot абстрагирует порт, но, например, Micrometer до недавнего времени не собирал все те же метрики для Jetty, что для Tomcat, – приходилось доконфигурировать). Jetty historically имел менее строгую реализацию JSP (часть девелоперов жаловалась на незначительные отличия). Также Jetty, как и Tomcat, классически поток-ориентированный (NIO), так что особых преимуществ в concurrency нет (кроме, опять же, Loom скоро уравняет их). 
  • Производительность: В бенчмарках Jetty примерно равен Tomcat. Производительность Tomcat, Jetty и Undertow сопоставима, Undertow чуть быстрее, Jetty чуть медленнее Undertow”. Разница измеряется <10%. Иногда Jetty показывает лучше latency под небольшой нагрузкой.
  • Когда выбирать: Jetty может быть предпочтителен, если у вас приложение, требующее быстрого старта и малого memory footprint (например, serverless функции на Java – там Jetty часто используют). Также если у вас уже DevOps-культура заточена под Jetty или есть tooling (кастомные Valve/Patch, etc.), тогда есть смысл. Но обычно причина – “не люблю Tomcat, хочу что-то полегче”.

JBoss Undertow

  • Описание: Undertow – современный веб-сервер от Red Hat, который был основой сервера приложений WildFly. Он разработан с прицелом на низкое потребление ресурсов и высокую производительность. Undertow может работать как стандартный Servlet-контейнер (реализует Servlet 4.0) и WebSocket, а может работать в reactive-режиме (его native API – это non-blocking handlers, на основе callback’ов). Undertow в Spring Boot также поддерживается (starter-undertow). 
  • Преимущества: Легковесность – Undertow jar’ы суммарно меньше, кодовая база компактнее. Он показывает очень высокую производительность на синтетических тестах – нередко быстрее Tomcat на 5-10%. Кроме того, Undertow хорошо поддерживает HTTP/2, WebSocket, и может работать embedded в Quarkus (фреймворк, где Undertow используется с Mutiny (реактивно) для получения очень малого времени старта). 
  • Недостатки: Меньшая распространенность и сообщество. Менее богатая экосистема: например, не было своего JSP (WildFly использовал JSP от Tomcat Jasper). Spring Boot 2 имел пару issue с Undertow (например, одна версия Undertow had bug with HTTP2).
  • Горячая замена/разработка: Tomcat и Jetty традиционно позволяют reload context без перезапуска JVM (у Tomcat Manager, у Jetty deployer) – Undertow сам по себе не предоставляет, это задача WildFly. Но в Spring Boot devtools такие вещи на уровне Spring решаются, так что не критично. 
  • Когда выбирать: Undertow хорошо подходит, когда нужен максимум производительности от сервлетов при минимуме overhead. Например, для high-throughput API gateways или в средах, где каждая микросекунда latency важна. В Spring Boot переключение на Undertow даст +5% RPS – возможно, это незначительно. Но Undertow умеет интересное: сервлеты без потока на запрос – если использовать его native API, можно обрабатывать запросы асинхронно на небольшом пуле потоков (под капотом – XNIO). То есть Undertow out-of-the-box более “Netty” по духу. 

Netty (и Spring WebFlux с Reactor Netty): 

  • Описание: Netty – это не сервлет-контейнер, а асинхронный I/O фреймворк (NIO на стероидах). Он не поддерживает Servlet API – вместо этого предоставляет низкоуровневый pipeline для обработки событий сокета. Многие современные фреймворки на нем строят HTTP сервер (Vert.x, Spring WebFlux, gRPC Java). 
  • Преимущества: Netty может достичь очень высоких уровней конкурентности и throughput, благодаря событийному циклу. Например, для “Hello World” netty-сервер может выдать сотни тысяч RPS на мощной машине, тогда как Tomcat, будучи ограничен потоками, выдаст меньше (до Loom по крайней мере). Netty хорошо масштабируется на большое число соединений (поддержка epoll, kqueue). Он чрезвычайно гибок: можно реализовать свой протокол, добавить custom handlers (например, SSL termination, compression, etc). 
  • Недостатки: Писать непосредственно на Netty довольно сложно – API низкоуровневое, отладка ошибок (особенно concurrency issues) нетривиальна. Поэтому чаще его используют через абстракции (тот же Spring WebFlux). Но даже WebFlux – требует реактивного стиля программирования, что сложнее для большинства разработчиков (asynchronous, callback/Mono/Flux chain). Кроме того, Netty – “не по спецификации” – если у вас есть готовый WAR с сервлетами, на Netty его не развернешь. 
  • Tomcat vs Netty: Ранее, до Loom, говорить о масштабируемости, Netty выигрывал тем, что один OS-поток мог вести много запросов, а Tomcat вынужден был плодить потоки. Сейчас Loom стирает это различие. То есть Tomcat+Loom смогут обработать столько же concurrent запросов, что Netty, с той разницей, что Tomcat код остается императивным, а Netty – реактивным. Конечно, Netty все равно может иметь меньше overhead (виртуальный поток – это все равно объект и context), но порядок величин сблизился.
  • Когда Netty уместен: в специализированных случаях: например, если вы строите высокопроизводительный гейм-сервер с собственным протоколом поверх TCP/UDP – Netty вне конкуренции. Если нужно освоить каждый бит производительности, и вы готовы к сложной разработке, Netty даст инструменты. В веб-разработке, я бы сказал, Netty оправдан для Reactive stack (где тысячи concurrent clients с long-polling/websocket – Spring WebFlux/Netty может показать лучшее использование ресурсов, чем Spring MVC/Tomcat, хотя Loom здесь тоже может поменять расклад).

Из интересного: Jetty 12 поддерживает HTTP/3, Undertow частично (в WildFly tech preview), Tomcat – нет, придется ставить HAProxy/nginx перед ним, если нужен HTTP/3. Если для вас принципиален HTTP/3 (скажем, вы стримите медиа где QUIC дает +), то Tomcat пока не выбор напрямую.

Итоговое сравнение: 

  • Производительность: все три (Tomcat, Jetty, Undertow) близки. Undertow немного быстрее под нагрузкой, Jetty потребляет чуток меньше памяти. Netty (реактивный) может дать преимущество при огромном числе соединений, но ценой сложности разработки. 
  • Стабильность и зрелость: Tomcat > Jetty >= Undertow. Tomcat сверх-стабильный, Jetty тоже давно, Undertow моложе (но тоже уже более 8 лет в деле, вполне надежный). 
  • Сообщество и поддержка: Tomcat лидирует. Про него масса статей, ответов на SO. Jetty тоже довольно известен. Undertow – нишевее, но его поддерживает Red Hat (RHEL EAP), так что корпоративная поддержка есть. 
  • Функционал: Tomcat и Jetty реализуют полный спектр Jakarta EE Web Profile (Servlet/JSP/WebSocket etc), Undertow – без JSP (но WildFly компенсирует). Если вам нужны специфичные Valve, realms – Tomcat здесь самый проработанный. 
  • Встраиваемость: все трое поддерживаются Spring Boot. Boot делает switching очень простым (исключить tomcat starter, добавить jetty/undertow starter). Если у вас другой фреймворк – Dropwizard historically на Jetty, Quarkus – на Undertow, Vert.x – свой web server на Netty. Каждый стек со своим “любимчиком”.
  • Особые возможности: Jetty – HTTP/3, Undertow – async API / embed in GraalVM native-image (Quarkus). Tomcat – крупный план Loom, а также у Tomcat классная фича: Apache APR/native – можно подключить libtcnative, и Tomcat будет использовать OpenSSL и native poll, что ускоряет TLS и стабильно работает на старых Java8 (Jetty, Undertow тоже умеют native TLS, но Tomcat первым это сделал).

В заключение, Tomcat остается “рабочей лошадкой” – универсальной и надежной. Jetty и Undertow – отличные альтернативы для отдельных случаев, но часто бенефиты незначительны. Netty/Vert.x – для совсем другого (реактивного) подхода. С Loom даже эта грань стирается: можно получить производительность Netty, оставаясь на знакомом Tomcat+Servlet API, что, вероятно, приведет к еще большему доминированию Tomcat (или по крайней мере к сохранению его лидерства).

Подводные камни и практические советы

Несмотря на зрелость Tomcat, в эксплуатации возникают типичные ошибки и проблемы, о которых стоит помнить. Ниже перечислены “подводные камни” и советы, как их обходить, основанные на опыте продакшн-развертывания Tomcat:

1. Неправильная конфигурация потоков и пулы ресурсов. Часто разработчики, деплоив приложение на Tomcat, забывают настроить параметры под боевую нагрузку. По умолчанию maxThreads=200 – для большинства случаев ок, но, например, если ваше приложение очень легкое (скажем, CPU-bound), 200 потоков могут даже избыточны и приводить к лишнему контекст-свитчингу. Или наоборот, если вы ожидаете 1000 параллельных длинных запросов, 200 потоков не хватит – остальные будут ждать в очереди, что может вызвать таймауты у клиентов. Совет: проводить нагрузочное тестирование и тюнинг. Подберите maxThreads так, чтобы CPU серверов использовался ~80% на пике, но не было очереди (или была минимальна). Аналогично с JDBC-пулом: Tomcat JDBC-пул (или Hikari) часто остается с дефолтом maxActive=100. Если у вас 200 потоков, а пул 100 – может возникнуть взаимное блокирование: потоки исчерпали пул и ждут свободного коннекта, а БД подтупливает – все 200 потоков повисли на ожидании, новые запросы в очередь – получаем стазис. Совет: размер пула БД >= (время БД / время сервиса) * потоков. Обычно 100 хватает, но если один запрос БД быстрый (0.01с), а сервис 0.5с, можно и 20 коннектов. Use metrics! (actuator hikaricp.connections.active поможет).

2. Потенциальные дедлоки из-за синхронизации в веб-приложении. Сам Tomcat старается дедлоков избегать (и случаев взаимной блокировки потоков на своих ресурсах не зафиксировано), но приложения могут внести. Например: 

  • Использование synchronized на общих объектах (случай: synchronized на HttpSession объекте – иногда делают, думая что так обезопасят concurrent доступ. Но Tomcat уже сериализует доступ к сессии, так что это не нужно – а двойная синхронизация может привести к deadlock, если, допустим, filter пытается получить ту же сессию). 
  • Запуск внутри сервлета долгих задач, которые ожидают результата от другого запроса (классика: Servlet A делает HTTP-запрос к самому себе (localhost:8080) – в результате все потоки заняты такими запросами друг к другу, ничего не выполняется – thread starvation deadlock). Совет: никогда не делайте внешних запросов к тому же контейнеру синхронно из запроса. Если нужен меж-сервлетный вызов, вызывайте код напрямую или через async message/queue.
  • Два сервлета, держащие lock друг на друге (A вызывает B и держит lock, B вызывает A – циклическое ожидание). При подозрении на дедлок – снять thread dump (jstack). Если видите “waiting on <monitor>” у многих потоков, и один “in synchronized” – сигнал.

3. Утечки памяти при редеплое (redeploy). В среде разработки, когда часто redeploy WAR на Tomcat без рестарта JVM, можно столкнуться с сообщениями типа: “Warning: web application [XYZ] appears to have started a thread named [Foo] but has failed to stop it… possible memory leak.”. Это Tomcat детектирует, что приложение не остановило свои background-потоки. Классика – забудут остановить ScheduledExecutorService или Timer. Совет: всегда в контекст-листенере или Servlet.destroy() останавливать фоновые потоки. Tomcat постарается убрать утечку (например, прибьет JDBC-драйвер), но потоки он не прибивает (кроме явного stop() undeploy, и то). Другой вид утечки – статические коллекции держат ссылки на классы приложения. Например, регистрировали какой-нибудь com.myapp.MyListener в static поле библиотечного класса – после undeploy MyListener класс не выгружается. Совет: избегать статических singletons, особенно содержащих ClassLoader-специфичные объекты (JDBC DriverManager – как раз такой, Tomcat принудительно чистит). В log Tomcat как раз пишет “forcibly unregistered to prevent memory leak” – это сигнал, что ваш код не почистил за собой, но Tomcat помог. Лечится чтением документации и настройкой: например, JreMemoryLeakPreventionListener Tomcat’а уже делает много автоматом (подгружает драйвер, прочие hack’и). В dev-среде можно использовать Parallel deployment (Tomcat поддерживает версионирование контекстов) или просто перезапускать Tomcat – это уж точно очистит память.

4. Недостаточные таймауты и задержки закрытия соединений. По умолчанию connectionTimeout=20s – если клиент за 20 сек не прислал полный запрос, Tomcat дропнет соединение. Для web-pages 20s – даже много. Но если у вас slow clients (например, IoT устройства по медленному GPRS) – может понадобиться увеличить. Наоборот, если short polling – лучше уменьшить, чтобы не держать соединения. KeepAliveTimeout (по умолчанию равен connectionTimeout) – важно, чтобы он не был слишком большим, иначе куча keepalive connections висят без дела. Лучше выставлять keepAliveTimeout (в <Connector>), скажем 5-10 секунд. Тогда idle соединения быстро закрываются, не занимая maxConnections. Еще: если Tomcat за proxy – нужно настроить proxyTimeout там, иначе proxy может раньше рвать соединение, чем Tomcat, вызывая “unexpected EOF”. Совет: согласовывать таймауты на всех уровнях: proxy (nginx) – Tomcat – upstream.

5. Неправильно настроенные Reverse Proxy Headers. Часто Tomcat работает за nginx/Apache httpd. В таких случаях, Tomcat может видеть все запросы от 127.0.0.1 (т.к. proxy). Чтобы приложение получало реальный IP клиента и корректную scheme (https/http), нужно настроить RemoteIpValve. Многие забывают, и потом лог IP = 127.0.0.1, или redirect генерируется на http:// вместо https://. Совет: включить RemoteIpValve (в Boot – server.forward-headers-strategy=framework), либо настроить proxy на модификацию X-Forwarded-For. Это не проблема Tomcat как такового, но распространенный misconfig.

6. Безопасность на продакшене: 

  • Нередко оставляют включенным Tomcat Manager или Default examples на прод. Это небезопасно: Manager надо защищать сильным паролем или отключить совсем если не нужен, а приложения examples/ лучше не деплоить на прод (в них исторически находили уязвимости). 
  • Не меняют Shutdown порт 8005 – теоретически, если злоумышленник имеет доступ к TCP 8005, он может послать строку “SHUTDOWN” и выключить Tomcat. Лучше отключить (установить port=”-1″ в Server элемент). 
  • Запускают Tomcat под root – категорически нет (кроме, разве что, Windows Service, где нет root). Всегда юзер с минимум привилегий, как рекомендует Apache. 
  • Не обновляют Tomcat вовремя: Tomcat регулярно выпускает security fixes (CVE). Надо следить. Например, GhostCat (CVE-2020-1938) – серьезная уязвимость, требующая обновления min Tomcat 9.0.31. Многие тянули и подставились. Совет: иметь план регулярных обновлений Tomcat (благо в Boot Gradle/Maven это легко управляется версией dependency).

7. Проблемы с загрузкой классов и драйверов. Иногда наблюдается “дублирование” библиотек: например, кладут JDBC-драйвер и в Tomcat lib, и в веб-приложение lib. Это может привести к странным багам (два драйвера регистрируются). Или ситуация: webapp включает Log4j, а Tomcat juli тоже пишет – конфликты JAR версий. Совет: по возможности, использовать общий логгер (JULI) или правильный bridging. И не пихать в WEB-INF/lib то, что уже есть в Tomcat lib – Tomcat при старте пишет предупреждения о дублирующих JAR’ах. Отдельно: драйверы JDBC – Tomcat автоматически найдет JDBC4.0-драйверы и зарегистрирует. Если вы не хотите чтобы Tomcat управлял – можно отключить Jmx for DataSource, но обычно пусть делает. Главное, при undeploy – Tomcat сам убирает драйверы (показывая сообщение “forcibly unregistered”). В принципе, memory leak предотвращен, но warning пугает. Совет: чтобы не было warning, либо не делать undeploy, либо явно deregister DriverManager в ServletContextListener.contextDestroyed.

8. Логи Tomcat переполняют диск. По умолчанию Tomcat rotatelog daily. Однако, если приложение очень активно логирует (stdout -> catalina.out), файл может разрастись. Совет: использовать логирование через SLF4J + JULI adapter и настроить ротацию/удаление старых. Spring Boot, кстати, перехватывает stdout, делает rolling log (springboot logback config), так что в Boot embedded Tomcat проблемы нет. А вот standalone Tomcat – надо следить (rotate catalina.out, host-manager.log etc).

9. Ошибки, связанные с кодировкой. Tomcat по умолчанию считает URL и параметры в UTF-8 (URIEncoding=”UTF-8″ на Connector). Но некоторые старые системы ожидают ISO-8859-1. Или бывает, JSON ресурс прилетел, Content-Type без charset – Tomcat считает iso-8859-1, что неправильно. В general, Tomcat 10 default charset is UTF-8, так что проблем меньше. Но на границах, e.g. Tomcat reading environment variables (JNDI), locale, etc – может что-то выпасть. Совет: явно указывать charset везде (в HTML forms, etc.). Tomcat provides AddDefaultCharsetFilter – в конфиге веб приложений можно добавить, чтобы защита от XSS через charset sniffing.

10. Отладка на продакшене без воздействия на работу. Иногда нужно повесить профайлер или сделать heap dump, но Tomcat worker threads постоянно бегают. Один из приемов: Servlet 3.0 async можно использовать, чтобы “заморозить” прием новых запросов (просто как идея). Но проще – перевести нагрузку на другие ноды, а на одной включить Runtime.getRuntime().halt() после снятия дампа – но это крайний случай. В общем, Tomcat самодостаточен, но админы должны уметь пользоваться jstack, jmap, JConsole. Tomcat, запущенный как Windows Service, сложнее дебажить – лучше запускать через service manager UI (там есть thread dump option). И еще подводный камень: “Tomcat не останавливается (не завершается)” – обычно случается, когда webapp оставил non-daemon threads. Tomcat ждет, но если threads помечены как user threads, JVM не выходит. В log пишут “Tomcat did not stop in 45 seconds, will forcibly terminate”. Совет: в development environment, если что, kill процесс. А на проде – избегайте создавать постоянные потоки, лучше ThreadPoolExecutor с setDaemon(true) threads, или Timer as daemon.

В заключение этого раздела: многие “подводные камни” обусловлены не Tomcat как таковым, а использованием его по умолчанию без учета особенностей продакшн-нагрузки. Поэтому важный совет – тщательно тестировать в условиях, близких к боевым, мониторить и читать лог-файлы. Tomcat обычно сообщает о проблеме в логах (будь то подозрение на утечку, или порт занят, или misconfiguration). Настройте оповещения на critical сообщения. Помните, что Tomcat – весьма надежный сервер, и большинство проблем возникают либо из-за ошибок приложения, либо неправильной конфигурации/окружения. Зная типичные подводные камни, вы сможете избежать простоев и обеспечить стабильную работу вашего сервера.

Заключение

Почему знание Tomcat критично для Java-разработчика? Несмотря на обилие абстракций (Spring Boot скрывает контейнер, облачные платформы могут автоматически управлять сервером), понимание работы Tomcat “под капотом” напрямую влияет на вашу эффективность при отладке и оптимизации. Когда приложение “тормозит” или “падает” в продакшене, часто именно глубокое знание сервера помогает быстро выявить причину. Например, зная устройство пула потоков Tomcat, вы сразу заподозрите thread starvation при росте latency и увидев busy_threads=200/200. Или, зная про maxHttpHeaderSize, вы быстро найдете причину 400 Bad Request на слишком большие cookies. Без этих знаний разработчик мог бы потратить много времени в поиске.

Tomcat – не черный ящик, а прозрачный инструмент: метрики, логи, конфиги – все доступно. Разработчик, знакомый с ними, становится сам себе DevOps: может настроить сервер под нужды приложения, а не наоборот. Это особенно актуально для middle/senior специалистов, которым доверяют не только писать код, но и отвечать за его работу в продакшене.

Знание архитектуры Tomcat помогает писать более эффективный код. Например, понимая, что каждый запрос – это поток (в классическом модели), вы будете осторожнее с блокирующими операциями внутри doGet(). Или зная, как Tomcat переиспользует объекты Request/Response, вы не будете хранить в них данные, живущие дольше запроса. А понимание жизненного цикла Servlet (инициализация один раз, многопоточное выполнение service) убережет от ошибок синхронизации и инициализации.

В эпоху микросервисов и облаков, Tomcat зачастую скрыт внутри контейнеров, но проблемы остались те же: “OutOfThreads”, “OutOfMemory”, “Stuck threads”, “Slow responses” – и разбираться в них нужно все так же. Кто-то может сказать: “Мы используем AWS Lambda, там нет Tomcat” – но там есть другой HTTP runtime, с похожими принципами. Servlet-контейнеры лежат в основе большинства Java-бэкендов, так что их понимание универсально.

Как эти знания помогают в диагностике и оптимизации? Приведем несколько реальных сценариев: 

  • Приложение периодически “подвисает” на 1-2 минуты. Зная Tomcat, вы сразу подозреваете “Full GC” (остановка мира) или thread deadlock. Снимаете jstack – видите, что все 200 threads waiting on DB connection. Смотрите метрики – numActive БД-пула = 100 (max), ThreadsBusy = 200. Вывод: пул кончился, потоки ждут. Решение: увеличить пул или уменьшить синхронность запросов. Без знаний Tomcat, можно долго грешить “на сетевой лаг” или что-то еще.
  • Клиенты получают много 502/504 от Nginx, в логах Tomcat ничего нет. Знающий инженер вспомнит acceptCount. Возможно, Tomcat был перегружен и не принял соединения – а Nginx вернул 502 Bad Gateway. Подняв acceptCount и maxThreads, проблема уйдет. Без этого знания, можно было обвинять Nginx или сеть. 
  • Развернули новую версию, через пару часов – OutOfMemory. Tomcat знаток посмотрит heap dump: видит, что много экземпляров класса драйвера БД. Ага, значит при каждом деплое новый драйвер регается, старый не убран (memory leak). Проверит логи – точно: “JDBC driver forcibly unregistered” warning был. Решение: перезапускать Tomcat при каждом деплое или использовать Tomcat JNDI DataSource, чтобы драйвер жил на уровне контейнера. Без знаний, стали бы “увеличивать память”. 
  • Требуется снизить время отклика страницы. Опытный разработчик вспомнит про сжатие (compression). В Tomcat достаточно включить compression=”on” и настроить порог – и HTML/JSON начнут отправляться сжатыми, экономя время на сетевую передачу. Это чисто настройка сервера, код менять не надо – но это знает тот, кто читал доку Tomcat (или Spring Boot property server.compression.enabled=true). Такие маленькие оптимизации (HTTP/2 включить, TLS session cache настроить, etc.) отличают “так себе прод” от “быстрого прод”. 
  • Обнаружили, что одно из веб-приложений плохо работает при высокой нагрузке, остальные на том же Tomcat – нормально. Зная архитектуру, инженер догадается: все контексты разделяют общий пул потоков Engine. Если одно приложение забило все потоки (например, виснет на I/O), то и остальные страдают, ожидая потока. Решение: либо изолировать приложения на разные Tomcat инстансы, либо настроить разные <Service> с отдельными <Executor> (Tomcat позволяет каждому коннектору свой пул или общий на несколько). Это тонкая настройка, но знание ее – мощный инструмент.
  • Безопасность: сканер нашел, что Tomcat отвечает заголовком “Server: Apache Tomcat/10.0”. С точки зрения секьюрити, лучше скрыть версию. Инженер знает, что Tomcat 10 позволяет задать server.removeAppProvidedValues=true и изменить серверный banner (через org.apache.catalina.util.ServerInfo, либо использовать Valve). Он настраивает – уходит потенциальная информация для хакера. Мелочь, но знания. 
  • Инцидент: кто-то случайно удалил полосу в server.xml, Tomcat не запустился. Умеющий читать логи Tomcat (commons-daemon log) быстро поймет по stacktrace, где синтаксическая ошибка. Незнающий – будет переустанавливать Tomcat или тратить время.

Понимая внутреннее устройство Tomcat, вы можете гораздо быстрее диагностировать проблемные состояния, оптимизировать использование ресурсов и принимать грамотные архитектурные решения. Разработчик всегда видит картину шире: он понимает, что под 1000 RPS нужно не только хороший код написать, но и сервер настроить, и мониторинг.

Loading