HAProxy для высоконагруженного e-commerce: балансировка 50 000 одновременных пользователей

Почему HAProxy, а не Nginx

Интернет-магазин «МегаШоп» готовился к распродаже и ожидал 50 000 одновременных пользователей. Существующая инфраструктура — один Nginx, проксирующий на два бэкенда — не справлялась уже при 15 000 пользователях. Клиент спросил: «Может, просто добавить серверов за Nginx?» Мы ответили: «Серверы добавим, но балансировщик заменим на HAProxy».

HAProxy и Nginx — оба отличные инструменты, но для чистой L7-балансировки HAProxy выигрывает по нескольким критериям:

КритерийHAProxyNginx
Алгоритмы балансировки12+ (roundrobin, leastconn, source, uri, hdr, rdp-cookie...)5 (round-robin, least_conn, ip_hash, hash, random)
Health checksL4/L7, TCP, HTTP, send/expect, межинтервальные, agent checksТолько TCP и HTTP (расширенные — в Plus)
Stick tablesВстроенные, синхронизация между нодами, rate limitingНет аналога (нужны модули)
СтатистикаДетальная real-time страница, API, Prometheus exporterБазовая (расширенная — в Plus)
Hot reloadБез потери соединений (hitless reload)С потерей keepalive-соединений
Статический контентНе умеетОтлично

Вывод: HAProxy — специализированный балансировщик с богатыми возможностями, Nginx — универсальный веб-сервер с функцией балансировки. Для задачи «МегаШопа» HAProxy был идеальным выбором.

Архитектура решения

Мы спроектировали следующую схему:

  • 2 HAProxy (active/standby через Keepalived) — виртуальный IP 10.0.0.100.
  • 4 backend-сервера — приложение на Node.js, каждый обслуживает до 15 000 соединений.
  • 2 сервера для статики — Nginx раздаёт CSS/JS/images, кешированные в памяти.
  • SSL termination на HAProxy — бэкенды работают по HTTP, экономя CPU.

Установка HAProxy 2.8 LTS на Ubuntu 22.04:

# Репозиторий HAProxy с актуальной версией
apt install -y software-properties-common
add-apt-repository -y ppa:vbernat/haproxy-2.8
apt update && apt install -y haproxy=2.8.*

# Проверка
haproxy -v
# HAProxy version 2.8.5-1ppa1~jammy 2024/02/15

# Включение автозапуска
systemctl enable haproxy

Полная конфигурация haproxy.cfg

Приводим полный конфиг с комментариями:

global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    # Производительность
    maxconn 100000
    nbthread 4

    # SSL
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    tune.ssl.default-dh-param 2048

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    option  forwardfor
    option  http-server-close
    timeout connect 5s
    timeout client  30s
    timeout server  30s
    timeout http-request 10s
    timeout http-keep-alive 5s
    timeout queue 30s
    errorfile 503 /etc/haproxy/errors/503.http

    # Ретраи
    retries 3
    option  redispatch
    retry-on all-retryable-errors

# ==========================================
# FRONTEND: приём входящего трафика
# ==========================================

frontend fe_http
    bind *:80
    # Редирект HTTP → HTTPS
    http-request redirect scheme https code 301 unless { ssl_fc }

frontend fe_https
    bind *:443 ssl crt /etc/haproxy/certs/megashop.ru.pem alpn h2,http/1.1
    mode http

    # HTTP/2 — автоматически через alpn

    # ACL — определяем тип запроса
    acl is_static path_beg /static /assets /images /favicon.ico
    acl is_api    path_beg /api/
    acl is_ws     hdr(Upgrade) -i websocket

    # Rate limiting через stick table
    stick-table type ip size 200k expire 30s store http_req_rate(10s),conn_cur
    http-request track-sc0 src
    # Блокируем IP с более чем 100 запросами за 10 секунд
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
    # Блокируем IP с более чем 50 одновременными соединениями
    http-request deny deny_status 429 if { sc_conn_cur(0) gt 50 }

    # Маршрутизация по ACL
    use_backend be_static   if is_static
    use_backend be_websocket if is_ws
    use_backend be_api       if is_api
    default_backend be_app

# ==========================================
# BACKENDS: пулы серверов
# ==========================================

backend be_app
    balance leastconn
    option httpchk GET /health HTTP/1.1\r\nHost:\ megashop.ru
    http-check expect status 200

    # Cookie-based persistence (корзина покупок)
    cookie SERVERID insert indirect nocache maxlife 30m

    # Медленный старт — новый сервер получает нагрузку плавно
    default-server inter 3s fall 3 rise 2 slowstart 60s

    server app1 10.0.1.1:3000 check cookie app1 weight 100 maxconn 15000
    server app2 10.0.1.2:3000 check cookie app2 weight 100 maxconn 15000
    server app3 10.0.1.3:3000 check cookie app3 weight 100 maxconn 15000
    server app4 10.0.1.4:3000 check cookie app4 weight 100 maxconn 15000

backend be_api
    balance roundrobin
    option httpchk GET /api/health HTTP/1.1\r\nHost:\ megashop.ru
    http-check expect status 200

    # API — без session stickiness, stateless
    default-server inter 2s fall 2 rise 2
    server api1 10.0.1.1:3000 check maxconn 10000
    server api2 10.0.1.2:3000 check maxconn 10000
    server api3 10.0.1.3:3000 check maxconn 10000
    server api4 10.0.1.4:3000 check maxconn 10000

backend be_static
    balance roundrobin
    option httpchk GET /health HTTP/1.1\r\nHost:\ megashop.ru
    http-check expect status 200

    http-response

Health checks и connection queuing

HAProxy проверяет здоровье бэкендов тремя способами:

  • L4 TCP check — просто открывает TCP-соединение. Быстро, но не ловит зависшее приложение.
  • L7 HTTP check — отправляет HTTP-запрос и проверяет ответ. Мы используем именно этот вариант.
  • Agent check — внешний скрипт на бэкенде сообщает HAProxy свой статус и вес.

Наш health check endpoint на Node.js:

// health.js — эндпоинт для HAProxy health check
app.get('/health', async (req, res) => {
  try {
    // Проверяем подключение к БД
    await db.query('SELECT 1');

    // Проверяем Redis
    await redis.ping();

    // Проверяем свободную память (не менее 100 МБ)
    const freeMemMB = os.freemem() / 1024 / 1024;
    if (freeMemMB < 100) {
      return res.status(503).json({ status: 'degraded', reason: 'low memory' });
    }

    // Проверяем Event Loop Lag (не более 100 мс)
    if (eventLoopLag > 100) {
      return res.status(503).json({ status: 'degraded', reason: 'event loop lag' });
    }

    res.status(200).json({ status: 'ok', uptime: process.uptime() });
  } catch (err) {
    res.status(503).json({ status: 'error', reason: err.message });
  }
});

Параметры inter 3s fall 3 rise 2 означают: проверять каждые 3 секунды, считать сервер упавшим после 3 неудач, считать восстановленным после 2 успехов. Суммарное время обнаружения сбоя — 9 секунд.

Connection queuing — важная функция HAProxy, отсутствующая в Nginx. Когда все соединения к бэкенду заняты (maxconn 15000), HAProxy не отклоняет запрос, а ставит его в очередь на timeout queue 30s. Это позволяет выдержать кратковременные пики без ошибок 503.

Stick tables и rate limiting

Stick tables — уникальная возможность HAProxy, позволяющая хранить данные о клиентах (IP, cookie, header) прямо в памяти балансировщика без внешних хранилищ.

Мы использовали stick tables для трёх задач:

1. Rate limiting (защита от DDoS и парсеров):

# В секции frontend:
stick-table type ip size 200k expire 30s store http_req_rate(10s),conn_cur,gpc0

# Трекаем каждый IP
http-request track-sc0 src

# Мягкий лимит — 100 req/10s → 429
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

# Жёсткий лимит — 500 req/10s → бан на 10 минут
http-request sc-set-gpc0(0) 1 if { sc_http_req_rate(0) gt 500 }
acl is_banned sc_get_gpc0(0) gt 0
http-request deny deny_status 403 if is_banned

2. Session persistence (привязка к серверу):

# В backend — привязка по cookie
backend be_app
    stick-table type string len 52 size 100k expire 30m
    stick store-response res.cook(PHPSESSID)
    stick match req.cook(PHPSESSID)

3. Мониторинг stick tables в реальном времени:

# Просмотр содержимого stick table
echo "show table fe_https" | socat stdio /run/haproxy/admin.sock

# Пример вывода:
# 0x55a8c7f23420: key=192.168.1.50 use=0 exp=24312 http_req_rate(10000)=42 conn_cur=3 gpc0=0
# 0x55a8c7f24680: key=45.33.22.11  use=0 exp=1523  http_req_rate(10000)=847 conn_cur=87 gpc0=1

# Ручной бан IP
echo "set table fe_https key 45.33.22.11 data.gpc0 1" | socat stdio /run/haproxy/admin.sock

# Разбан IP
echo "clear table fe_https key 45.33.22.11" | socat stdio /run/haproxy/admin.sock

На распродаже stick tables автоматически заблокировали 340 IP-адресов парсеров и 12 IP ботнета, которые пытались положить сайт. Всё — без внешних WAF и дополнительных сервисов.

SSL termination и hot reload

SSL termination на HAProxy экономит до 30% CPU на бэкендах. Настройка:

# Подготовка сертификата — HAProxy требует PEM с ключом и сертификатом в одном файле
cat /etc/letsencrypt/live/megashop.ru/fullchain.pem \
    /etc/letsencrypt/live/megashop.ru/privkey.pem \
    > /etc/haproxy/certs/megashop.ru.pem
chmod 600 /etc/haproxy/certs/megashop.ru.pem

# Автообновление сертификата (cron)
0 3 * * * certbot renew --deploy-hook "cat /etc/letsencrypt/live/megashop.ru/fullchain.pem /etc/letsencrypt/live/megashop.ru/privkey.pem > /etc/haproxy/certs/megashop.ru.pem && systemctl reload haproxy"

Hot reload — HAProxy перечитывает конфигурацию без потери соединений:

# Проверка конфигурации перед reload
haproxy -c -f /etc/haproxy/haproxy.cfg
# Configuration file is valid

# Reload без потери соединений
systemctl reload haproxy

# Что происходит при reload:
# 1. Запускается новый процесс HAProxy с новой конфигурацией
# 2. Новый процесс принимает новые соединения
# 3. Старый процесс дообслуживает существующие соединения
# 4. После закрытия всех соединений старый процесс завершается

# Мониторинг reload через stats socket
echo "show proc" | socat stdio /run/haproxy/admin.sock

За время распродажи мы делали reload 4 раза (добавление бэкендов, изменение rate limits) — ни одного обрыва соединения.

Keepalived для отказоустойчивости HAProxy

Один HAProxy — single point of failure. Мы развернули два HAProxy с Keepalived для автоматического переключения виртуального IP:

# /etc/keepalived/keepalived.conf — MASTER (haproxy1)
vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -20
    fall 3
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 110
    advert_int 1
    unicast_src_ip 10.0.0.1
    unicast_peer {
        10.0.0.2
    }
    authentication {
        auth_type PASS
        auth_pass megashop_vrrp_2025
    }
    virtual_ipaddress {
        10.0.0.100/24
    }
    track_script {
        chk_haproxy
    }
    notify_master "/etc/keepalived/notify.sh MASTER"
    notify_backup "/etc/keepalived/notify.sh BACKUP"
    notify_fault  "/etc/keepalived/notify.sh FAULT"
}
# /etc/keepalived/keepalived.conf — BACKUP (haproxy2)
vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 100
    # ... остальное идентично
}

При падении HAProxy на master-ноде Keepalived обнаруживает это за 6 секунд (interval 2 * fall 3) и переключает VIP на backup. Клиенты не замечают переключения — TCP-соединения пересоздаются через браузер автоматически.

Результаты и рекомендации

Распродажа прошла без единого 503 при пиковых 52 000 одновременных пользователях:

МетрикаNginx (до)HAProxy (после)
Макс. одновременных пользователей15 00052 000
P99 latency2800 мс340 мс
Ошибки 50312% при пике0%
Время обнаружения сбоя бэкенда30 с (passive)9 с (active)
Заблокировано ботов0352 IP
Время failover HAProxyN/A6 секунд

Рекомендации по HAProxy:

  • Всегда используйте option httpchk вместо TCP check — падение приложения при работающем TCP-порте будет обнаружено только HTTP check.
  • Включайте maxconn на серверах — это предотвращает перегрузку бэкенда и активирует connection queuing.
  • Stick tables заменяют внешний rate limiter для 90% сценариев — не тратьте деньги на Cloudflare при обычных нагрузках.
  • Keepalived — обязательный компонент, настройка занимает 30 минут, а защищает от полного простоя.

Часто задаваемые вопросы

Если вам нужен только балансировщик — HAProxy. Если нужен веб-сервер с функцией балансировки (раздача статики + проксирование) — Nginx. HAProxy превосходит Nginx в алгоритмах балансировки, health checks, stick tables и hot reload. Nginx удобнее, когда балансировщик совмещён с раздачей статики и SSL. Идеальная схема: HAProxy на входе для балансировки, Nginx на бэкендах для статики.
HAProxy автоматически определяет WebSocket по заголовку Upgrade: websocket и переключает соединение в туннельный режим. Важно увеличить timeout server и timeout tunnel до нужного значения (например, 3600s для часовых сессий). Для балансировки WebSocket используйте алгоритм source (привязка по IP), чтобы все соединения одного клиента попадали на один сервер.
Один инстанс HAProxy на сервере с 4 ядрами и 8 ГБ RAM стабильно обрабатывает 200 000-300 000 одновременных соединений. На рекордных тестах HAProxy обслуживал 2 миллиона одновременных соединений. Основные лимиты: maxconn в конфиге, ulimit -n в системе и объём RAM (каждое соединение потребляет ~30 КБ с SSL).
HAProxy 2.x имеет встроенный Prometheus exporter. Добавьте в конфиг: frontend prometheus bind *:8405 http-request use-service prometheus-exporter if { path /metrics }. В Prometheus scrape_configs добавьте target haproxy:8405. Ключевые метрики: haproxy_server_current_sessions, haproxy_backend_response_errors_total, haproxy_backend_http_responses_total. Готовый дашборд для Grafana — ID 12693.
HAProxy 2.x поддерживает обновление сертификата через Runtime API без reload: echo 'set ssl cert /etc/haproxy/certs/site.pem <

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#haproxy#load balancing#балансировка нагрузки#ssl termination#health check#stick tables#rate limiting#keepalived
Комментарии 0

Оставить комментарий

загрузка...