В одном из проектов, над которым мы сейчас работаем, мы имеем довольно типичную настройку с одним сервером (с Apache mod_rewrite
), передающим трафик на внутренние серверы.
У нас также есть некоторые автоматизированные тесты производительности / стресса. Вся система работала нормально, около 250 запросов в секунду, попадающих на сервер (http get / post обращения). Однако, когда мы увеличили это до 700 запросов в секунду, через некоторое время мы начали получать 503 ответа.
Как оказалось, эти запросы никогда не доходили до внутренних серверов, но журналы ошибок Apache содержали такие записи, как:
Cannot assign requested address attempt to connect to (...) failed
Погуглив некоторое время, выяснилось, что это может быть потому, что ОС (в данном случае Ubuntu) не смогла выделить новые порты. Каждому соединению клиент-сервер TCP назначается новый временный порт на стороне клиента, который обычно находится в диапазоне от 32768 до 61000. После завершения запроса, даже если обе стороны должным образом закроют соединение TCP, порт будет освобожден и повторно можно использовать только примерно через 4 минуты. Это связано с тем, что соединение переводится в состояние TIME_WAIT и будет сброшено по истечении соответствующего времени ожидания .
Так как соединение клиент-сервер TCP однозначно идентифицируется кортежем (клиентский IP, клиентский порт, IP-адрес сервера, порт сервера), при настройке по умолчанию сервер может обрабатывать только около 30 тыс. Запросов в течение 4 минут от одного клиента (сервера). адрес является фиксированным и общеизвестным, поэтому может изменить только клиентский порт).
Следующим шагом была проверка, действительно ли это проблема с распределением портов. Чтобы получить приблизительное количество открытых соединений, мы просто запустили во время тестов:
netstat -p tcp | wc -l
Мы также добавили grep
s на IP-адрес клиента или серверной части, чтобы подсчитать количество соединений между клиентской <-> прокси и прокси <-> серверной частью. Оказалось, что, хотя между клиентом и прокси-сервером существует постоянный пул соединений (поэтому HTTP keepalive работал правильно — см. Также ниже), для каждого запроса между прокси-сервером и бэкэндом было установлено новое соединение!
Таким образом, в нашем случае клиентская сторона TCP-соединения была прокси, серверная сторона — бэкендом, и ограничение на количество соединений, применяемых к прокси <-> бэкэнд-паре, даже если первоначально запросы могли поступать от различных клиентов. Следовательно, вся наша установка была ограничена 30 тыс. Запросов за 4 минуты на сервер.
Конечно, следующим шагом было выяснить, какая сторона инициирует закрытие соединений. Для этого мы использовали:
tcpdump src [proxy ip] and dst [backend ip]
и направил единичные запросы на сервер. Поток ясно показал, что Apache закрывает соединения.
Зачем? Это был очень хороший вопрос. Наша mod_rewrite
конфигурация Apache + была действительно простой:
<VirtualHost *:80> RewriteEngine On ProxyPreserveHost On RewriteRule ^(.*)$ http://[backend ip]:8080$1 [P,L] </VirtualHost>
Из-за отсутствия других потенциальных клиентов я обратился к ServerFault , что оказалось очень хорошей идеей. Я быстро получил ответ, что mod_rewrite
не делает пул соединений . Я не смог найти упоминаний об этом в документации, и я думаю, что это очень важно, особенно для систем с высокой нагрузкой.
Решение также было очень простым: используйте mod_proxy
вместо этого. Изменение вышеуказанного конфига на:
<VirtualHost *:80> ProxyPreserveHost On ProxyPass / http://[backend ip]:8080/ </VirtualHost>
вызвало то, что наши тесты наконец прошли под нагрузкой ~ 700 запросов / сек.
В качестве дополнительного примечания мы также убедились, что агент тестирования, отправляющий запросы (это был один компьютер), использует поддержку активности HTTP, что приводит к повторному использованию одного соединения TCP для нескольких запросов HTTP. Как выясняется, если вы используете Java — URLConnection
это не то, что straighforward (для простоты мы не использовали Apache HttpClient здесь): вам нужно настроить http.maxConnections
свойства системы , а не использовать .connect()
или .close()
методы на URLConnection
.