HAProxy 1.3 и тридцать строк конфига: как мы убрали узкое горло перед двумя app-серверами
Клиент-ретейлер упёрся в лимиты одного nginx-фронтенда. Внедрили HAProxy 1.3 в режиме TCP-балансировки - 30 строк конфига, автоматический failover без скриптов.
HAProxy 1.3 выходит как production-ready балансировщик нагрузки для Linux
В начале марта вышла HAProxy 1.3 с пометкой production-ready, и у нас как раз лежал свежий кейс, где этот инструмент попал точно в задачу.
Клиент - средний ретейлер, интернет-магазин на самописном PHP-приложении, два выделенных app-сервера. Несколько месяцев назад при росте трафика поставили перед ними nginx как фронтенд-прокси: отдавал статику, терминировал SSL, раздавал запросы на один app-сервер. Второй app-сервер фактически стоял в резерве и почти не использовался. Схема работала - до момента, когда трафик начал нормально расти вместе с рекламными кампаниями.
Где уперлись
При пиковой нагрузке картина была такая: nginx потреблял CPU под завязку, app-серверы при этом сидели вполовину загрузки. Узкое место оказалось не там, где его обычно ищут - не в базе, не в коде, а в единственной точке перед бэкендами. Nginx справлялся со статикой и SSL нормально, но сам по себе становился бутылочным горлом на проксировании динамики.
Второй app-сервер простаивал - потому что nginx был настроен на один upstream. Переписать конфиг nginx на два upstream можно, и мы это рассматривали. Но у nginx в тот момент не было встроенного мониторинга здоровья бэкендов с автоматическим исключением упавшего узла из ротации: если один app-сервер падал, nginx продолжал посылать туда запросы, и клиент видел ошибки до ручного вмешательства. Для ретейлера с живым магазином это неприемлемо.
Поэтому решили поставить перед nginx отдельный балансировщик с нормальным health check. Выбор пал на HAProxy.
Почему HAProxy, а не что-то другое
HAProxy 1.3 - специализированный балансировщик. Не веб-сервер, не прокси общего назначения, именно балансировщик. Это означает, что он делает одно и делает это хорошо: принимает соединения, проверяет живость бэкендов, распределяет нагрузку, выкидывает мёртвые узлы из ротации без посторонней помощи.
Нас интересовал режим TCP-балансировки: HAProxy принимает TCP-соединение и отправляет его на один из бэкендов по алгоритму round-robin или least-connections. В таком режиме он вообще не смотрит на HTTP - ему всё равно, что внутри. Значит, накладные расходы минимальные, а логика прозрачная.
Конкретно нас привлекло следующее:
- Health check из коробки. HAProxy периодически проверяет каждый бэкенд - простым TCP-соединением или HTTP-запросом. Если бэкенд не отвечает N раз подряд - выводится из ротации. Когда восстановился - возвращается. Без скриптов, без cron, без внешнего мониторинга в этой части.
- Статус-страница. HAProxy поднимает простую HTTP-страницу со статусом всех бэкендов - видно в реальном времени, кто живой, кто нет, сколько активных соединений.
- Конфиг читается. Не в смысле «короткий», а в смысле «понятный через полгода». Декларативный синтаксис, логика очевидна без комментариев.
Как выглядела схема
Итоговая топология простая:
Интернет -> HAProxy (порт 80/443) -> [nginx #1, nginx #2]
Точнее - у клиента два физических сервера. На каждом nginx + app. HAProxy встал на отдельной небольшой машине перед ними и слушал входящий трафик. Каждый nginx принимал соединения от HAProxy, отдавал статику, проксировал динамику в локальный PHP-FPM.
Весь конфиг HAProxy уложился примерно в 30 строк:
global
log 127.0.0.1 local0
maxconn 4096
daemon
defaults
log global
mode tcp
option tcplog
option dontlognull
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
listen stats
bind *:9000
mode http
stats enable
stats uri /haproxy-stats
frontend http-in
bind *:80
default_backend app-servers
backend app-servers
balance roundrobin
option tcp-check
server app1 192.168.10.11:80 check inter 2s fall 3 rise 2
server app2 192.168.10.12:80 check inter 2s fall 3 rise 2
Параметры health check: проверяем каждые 2 секунды, 3 последовательных неудачи - бэкенд выводится из ротации, 2 успешных подряд - возвращается. Это не магические числа - просто разумный баланс между скоростью обнаружения проблемы и нечувствительностью к единичным таймаутам.
Что показала практика
Запустили. Трафик распределился на оба app-сервера поровну. CPU на каждом из них - примерно вдвое ниже, чем раньше было на одном перегруженном. HAProxy при этом сам почти ничего не потребляет: его задача - принять соединение и передать его дальше, вся тяжёлая работа остаётся на nginx и app.
Падение одного из бэкендов проверили намеренно: остановили nginx на втором сервере. HAProxy через 6 секунд (3 проверки по 2 секунды) исключил его из ротации. Весь трафик пошёл на первый. Ни одной ошибки на клиентской стороне в этот момент не было - активные соединения достаивались, новые уходили на живой бэкенд. Это и есть то, ради чего ставился балансировщик.
Статус-страница на порту 9000 закрыта от внешнего доступа файрволом и доступна только из офисной сети. Полезная штука: можно одним взглядом видеть, оба ли бэкенда живые, сколько соединений на каждом.
Что осталось за кадром
HAProxy в TCP-режиме не видит HTTP-заголовки. Это значит, что sticky sessions - прибивание сессии пользователя к конкретному бэкенду - в таком режиме не работает. У данного клиента сессионное состояние хранится в базе, а не на app-сервере, поэтому вопрос не стоял. Но если бы сессии были локальными - нужен либо HTTP-режим HAProxy с cookie-based persistence, либо вынос сессий в общее хранилище. Это следующий разговор, не сегодняшний.
HAProxy в HTTP-режиме умеет больше: смотреть на заголовки, балансировать по URL, вставлять собственные заголовки. Но и сложнее. Для данной задачи TCP-режим был достаточен и проще в сопровождении.
Интеграция и запуск подобных схем - это то, что мы делаем в рамках проектных работ, а не только в рамках поддержки. Задача была конкретная, решение - прямолинейное. Двадцать минут на конфиг, несколько часов на тестирование и переключение трафика, ноль неожиданностей в момент запуска.