Статьи

Tomcat кусает больше, чем может жевать?


Это интересная запись в блоге для меня, так как она касается основной причины проблемы, с которой мы столкнулись в Tomcat еще в 
мае 2011 года , которая осталась нерешенной. При высокой степени параллелизма и нагрузки Tomcat будет сбрасывать (т. Е. RST уровня TCP) клиентские соединения, не отказываясь принимать их — как и следовало ожидать. Я снова опубликовал это в списке пользователей Tomcat 
несколько дней назад , но затем хотел выяснить основную причину для себя, так как она наверняка появится снова в будущем.

Фон

Первоначально эта проблема стала очевидной, когда мы провели тесты с высокой нагрузкой параллелизма в европейском клиентском офисе, где у клиента были развернуты внутренние службы на нескольких экземплярах Tomcat, и он хотел использовать UltraESB для маршрутизации сообщений с балансировкой нагрузки и переключением при сбое. Для 
теста производительности ESBМы использовали EchoService, написанный на библиотеке Apache HttpComponents / Core NIO, который очень хорошо масштабировался и хорошо работал на уровне TCP, даже под нагрузкой. Однако на клиентском сайте они хотели выполнить тестирование с реальными службами, развернутыми на Tomcat, — чтобы проанализировать более реалистичный сценарий. Для генерации нагрузки мы использовали клон ApacheBench на основе Java, называемый «Java Bench», который также является частью проекта Apache HttpComponents. Клиент перейдет на уровень параллелизма 2560, передавая как можно больше сообщений через ESB, для поддержки сервисов, развернутых через Tomcat.

При высокой нагрузке ESB начнет видеть ошибки во время разговора с Tomcat, и причиной будут ошибки ввода-вывода, такие как «Сброс соединения по одноранговой сети». Теперь проблема для ESB состоит в том, что он уже начал отправлять HTTP-запрос / полезную нагрузку по принятому TCP-соединению, и, таким образом, он не знает, может ли он по умолчанию безопасно переключаться на другой узел, поскольку внутренняя служба может выполнили некоторую обработку запроса, который он уже получил. Конечно, ESB также может быть настроен на повторную попытку при таких ошибках, но по умолчанию мы выполняем переключение при отказе только на более безопасном соединении, на которое было отказано, или ошибки тайм-аута соединения (т. Е. Соединение не может быть установлено в течение выделенного времени) — которая обеспечивает правильную работу даже для неидемпотентных услуг.

Недавние наблюдения

Недавно мы столкнулись с той же проблемой с Tomcat, когда клиент захотел выполнить сценарий нагрузочного тестирования, при котором серверная служба будет блокироваться случайным образом на 1-5 секунд, чтобы имитировать реалистичное поведение. Здесь мы снова увидели, что Tomcat сбрасывает принятые TCP-соединения, и мы смогли зафиксировать это с помощью Wireshark следующим образом, используя JavaBench напрямую против сервлета на основе Tomcat. 

Как видно из трассировки, клиент инициировал TCP-соединение с исходным портом 9386, а Tomcat, работающий через порт 9000, принял соединение — примечание «1». Клиент продолжал отправлять пакеты с запросом 100K, а Tomcat продолжал их подтверждать. Последний такой случай отмечен примечанием «2». Обратите внимание, что запрос клиента не был завершен в это время от клиента — примечание «3». Внезапно Tomcat сбрасывает соединение — примечание «4»

Понимание первопричины

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

import java.net.ServerSocket;
import java.net.Socket;

public class TestAccept1 {

  public static void main(String[] args) throws Exception {
  ServerSocket serverSocket = new ServerSocket(8280, 0);
  Socket socket = serverSocket.accept();
  Thread.sleep(3000000); // do nothing
  }
}

Мы просто открываем сокет сервера на порте 8280 с отставанием 0 и начинаем прослушивать соединения. Поскольку отставание равно 0, можно предположить, что будет разрешено только одно клиентское соединение — НО, я мог бы открыть больше, чем это через telnet, как показано ниже, и даже впоследствии отправить некоторые данные, набрав их и нажав клавишу ввода.
hello world

telnet localhost 8280
Команда netstat теперь подтверждает, что открыто более одного соединения:

netstat -na | grep 8280 
tcp  0  0 127.0.0.1:34629  127.0.0.1:8280  ESTABLISHED
tcp  0  0 127.0.0.1:34630  127.0.0.1:8280  ESTABLISHED
tcp6  0  0 :::8280  :::*  LISTEN 
tcp6  13  0 127.0.0.1:8280  127.0.0.1:34630  ESTABLISHED
tcp6  13  0 127.0.0.1:8280  127.0.0.1:34629  ESTABLISHED

Тем не менее, Java-программа приняла только ОДИН сокет, хотя на уровне ОС появятся два. Похоже, что ОС также позволяет открывать более двух подключений, даже если задание задано как 0. В Ubuntu 12.04 x64 команда netstat не будет показывать мне фактическую длину очереди прослушивания — но я считаю, что это не было 0. Однако до и после этого теста я не увидел различий в статистике переполнения «очереди прослушивания», которую можно было увидеть с помощью команды «netstat -sp tcp | fgrep listen».

Далее я использовал JavaBench из 
SOA ToolBox.  и выдал небольшую полезную нагрузку в параллельном режиме 1024 с одной итерацией для того же порта 8280

Как и ожидалось, все запросы не были выполнены, но моя трассировка Wireshark на порте 8280 не обнаружила сброса подключения. Повышение параллелизма до 2560 и итераций до 10 начало показывать RST уровня tcp — которые были аналогичны тем, которые видели на Tomcat, но не совсем так.

Может ли Tomcat сделать лучше?

Да, возможно .. Что конечный пользователь ожидает от Tomcat, так это от того, что он отказывается принимать новые соединения под нагрузкой и не принимать соединения, а затем сбрасывает их на полпути. Но можно спросить, достижимо ли это? Особенно учитывая поведение, которое мы видели на простом примере Java, который мы обсуждали.

Ну, решение могло бы заключаться в улучшении обработки низкоуровневых HTTP-соединений и сокетов, и это уже сделано с помощью высокопроизводительной бесплатной общедоступной корпоративной шины UltraESB с открытым исходным 
кодом , в которой используется превосходный 
 проект
Apache HttpComponents .

Как ведет себя UltraESB

Это можно легко проверить с помощью свойства stopNewConnectionsAt нашего слушателя NIO. Если вы установите его на 2, вы не сможете даже открыть сеанс Telnet для сокета за пределами 2.

Первый будет работать, второй тоже

Но третий увидит «Отказ в соединении»

И UltraESB сообщит следующее в своих журналах:

  INFO HttpNIOListener HTTP Listener http-8280 paused 
  WARN HttpNIOListener$EventLogger Enter maintenance mode as open connections reached : 2

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

Анализ соответствующего TCP-дампа

Чтобы проанализировать соответствующее поведение, мы написали простую прокси-службу Echo на UltraESB, которая также спала от 1 до 5 секунд, прежде чем она ответила, и проверили это с тем же JavaBench под 2560 одновременными пользователями, каждый из которых пытался отправить 10 сообщений в итерации.

Из 25600 запросов 7 были успешно выполнены, а 25593 завершились неудачно, как и ожидалось. Мы также видели много RST уровня tcp на дампе Wireshark, которые должны были быть выпущены базовой операционной системой.

Однако, что интересно отметить, это различие — RST возникают сразу после получения пакета SYN от клиента — и не являются установленными соединениями HTTP или TCP, а представляют собой элегантные ошибки «Отказано в соединении» — что может быть тем, чего клиент может ожидать. Таким образом, клиент может безопасно переключаться на другой узел без каких-либо сомнений, издержек или задержек.

Приложение: Поддержка высокого параллелизма в целом

Во время тестирования мы также увидели, что ОС Linux может обнаружить открытие множества одновременных подключений одновременно с 
 атакой
SYN , а затем начать использовать файлы cookie SYN. Вы бы увидели сообщения, такие как 


-> 


Возможно SYN-флуд на порт 9000. Отправка куки


отображается на выходе «sudo dmesg», если это произойдет.
Следовательно, для реальной загрузки было бы лучше отключить файлы cookie SYN, отключив их, как пользователь root


-> 


# echo 0> / proc / sys / net / ipv4 / tcp_syncookies


Чтобы сохранить изменения после перезагрузки, добавьте следующую строку в ваш /etc/sysctl.conf


-> 


net.ipv4.tcp_syncookies = 0

Чтобы позволить ОС Linux принимать больше подключений, также рекомендуется увеличить значение «net.core.somaxconn» — обычно по умолчанию оно равно 128 или около того. Это может быть выполнено пользователем root следующим образом:

->


# echo 1024> / proc / sys / net / core / somaxconn

Чтобы сохранить изменения, добавьте следующее в /etc/sysctl.conf

-> 


net.core.somaxconn = 1024

Престижность!

UltraESB не мог бы вести себя изящно без поддержки базовой 
библиотеки
Apache HttpComponents , а также помощи и поддержки, полученной от этого сообщества проекта, особенно от 
Олега Калничевского,  чей код и помощь всегда очаровывали меня!