Когда один сервер перестаёт справляться с трафиком, есть два пути: купить более мощную машину или распределить нагрузку между несколькими обычными. Второй вариант дешевле, гибче и надёжнее. Именно для этого существуют балансировщики нагрузки — HAProxy и Nginx upstream.

Оба умеют примерно одно и то же в общих чертах, но устроены совершенно по-разному. Разберём, как они работают, чем отличаются и когда что брать.

Зачем вообще нужна балансировка

Балансировщик нагрузки — это прокси, которая стоит перед твоими серверами (их называют backend или upstream) и раскидывает входящие запросы между ними по какому-то алгоритму.

Плюсы очевидны:

  • Горизонтальное масштабирование: добавил сервер — мощность выросла
  • Отказоустойчивость: один сервер упал — остальные продолжают работать
  • Zero-downtime деплой: обновляешь серверы по очереди, пользователи ничего не замечают
  • Равномерное распределение CPU и памяти

Это не rocket science, но настроить правильно — отдельная история.

Nginx как балансировщик: upstream-блок

Nginx все знают как веб-сервер и реверс-прокси. Но у него есть полноценный модуль балансировки — upstream.

Базовая конфигурация выглядит так:

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

server {
    listen 80;

    location / {
        proxy_pass http://backend;
    }
}

По умолчанию Nginx использует round-robin — запросы уходят к серверам по очереди. Просто и честно.

Алгоритмы балансировки в Nginx

round-robin — дефолт. Первый запрос на первый сервер, второй — на второй, и так по кругу.

least_conn — запрос уходит на сервер с наименьшим количеством активных соединений. Полезно когда запросы неравномерные по времени обработки.

upstream backend {
    least_conn;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

ip_hash — клиент всегда попадает на один и тот же сервер (на основе IP). Нужно когда сессии хранятся локально на сервере.

upstream backend {
    ip_hash;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

weights — можно задать вес серверу. Сервер с весом 3 получит в три раза больше запросов.

upstream backend {
    server 10.0.0.1:8080 weight=3;
    server 10.0.0.2:8080 weight=1;
}

Health checks в Nginx

В бесплатной версии Nginx health check пассивный: если сервер не ответил — Nginx помечает его как недоступный на какое-то время. Параметры:

upstream backend {
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
}

max_fails=3 — три неудачных попытки подряд, и сервер считается мёртвым. fail_timeout=30s — через 30 секунд Nginx снова попробует к нему обратиться.

Активные health checks (Nginx сам периодически опрашивает серверы) — только в Nginx Plus, это коммерческая версия. Для бесплатной есть сторонний модуль nginx_upstream_check_module, но его надо компилировать отдельно.

Backup-серверы

Можно пометить сервер как резервный — он получает трафик только если все основные серверы упали:

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080 backup;
}

HAProxy: специализированный балансировщик

HAProxy — это инструмент, который делает только одно, но делает это лучше всех. Он не умеет отдавать статику, не обслуживает PHP — он только балансирует.

Конфигурация HAProxy состоит из нескольких секций:

global
    log /dev/log local0
    maxconn 50000
    user haproxy
    group haproxy

defaults
    log     global
    mode    http
    option  httplog
    timeout connect 5s
    timeout client  30s
    timeout server  30s

frontend http_front
    bind *:80
    default_backend http_back

backend http_back
    balance roundrobin
    server web1 10.0.0.1:8080 check
    server web2 10.0.0.2:8080 check
    server web3 10.0.0.3:8080 check

Структура такая: frontend принимает входящий трафик, backend описывает группу серверов. Можно иметь несколько frontend'ов и backend'ов и маршрутизировать между ними по правилам.

Алгоритмы в HAProxy

HAProxy поддерживает больше алгоритмов, чем Nginx:

  • roundrobin — классический round-robin
  • leastconn — наименьшее число соединений (хорошо для долгих запросов)
  • source — по IP клиента (аналог ip_hash в Nginx)
  • uri — по URI (одинаковые URL всегда идут на один сервер, хорошо для кэширования)
  • hdr(name) — по значению HTTP-заголовка
  • random — случайный выбор
  • first — первый доступный сервер (экономит ресурсы при низкой нагрузке)

Health checks в HAProxy

Здесь главное преимущество. Слово check в конфигурации включает активную проверку прямо из коробки:

backend http_back
    balance roundrobin
    option httpchk GET /health
    server web1 10.0.0.1:8080 check inter 2s rise 2 fall 3
    server web2 10.0.0.2:8080 check inter 2s rise 2 fall 3
  • inter 2s — проверять каждые 2 секунды
  • rise 2 — 2 успешных ответа чтобы считать сервер живым
  • fall 3 — 3 неудачных ответа чтобы исключить сервер

option httpchk GET /health — HAProxy делает GET-запрос на /health и смотрит на HTTP-статус. Если 200 — сервер живой.

ACL и маршрутизация

Access Control Lists — одна из главных фич HAProxy. Можно маршрутизировать трафик по любым признакам:

frontend http_front
    bind *:80

    acl is_api path_beg /api/
    acl is_static path_end .jpg .png .css .js

    use_backend api_servers if is_api
    use_backend static_servers if is_static
    default_backend app_servers

API-запросы идут на одни серверы, статика — на другие, всё остальное — на основные. Nginx тоже умеет маршрутизацию через location, но ACL в HAProxy нагляднее для сложных сценариев.

Встроенная статистика

HAProxy имеет встроенный веб-интерфейс:

frontend stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:секретный_пароль

Открываешь браузер, видишь в реальном времени: количество запросов, какие серверы живые, количество ошибок, трафик в байтах. Nginx такого из коробки не даёт — только скромный stub_status.

HAProxy vs Nginx: честное сравнение

Критерий HAProxy Nginx
Производительность Выше (специализированный) Высокая
Health checks Активные из коробки Пассивные (активные — платно)
Протоколы HTTP, TCP, UDP HTTP, TCP (stream)
Маршрутизация Мощная ACL Через location
Статика Нет Да
Статистика Встроенная Только stub_status
SSL termination Да Да
Конфигурация Сложнее Проще

Если нужно просто распределить нагрузку на 2–3 сервера — Nginx справится. Если архитектура сложная, нужна детальная маршрутизация и мониторинг — HAProxy.

На практике часто используют оба: Nginx стоит первым (обрабатывает SSL, отдаёт статику, кэширует), за ним HAProxy балансирует динамику между серверами приложений.

Настройка SSL Termination

Принимать SSL на балансировщике и передавать трафик дальше по HTTP — стандартная практика. Это разгружает серверы приложений от шифрования.

В Nginx:

server {
    listen 443 ssl;
    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

В HAProxy:

frontend https_front
    bind *:443 ssl crt /etc/ssl/combined.pem
    default_backend http_back
    http-request set-header X-Forwarded-Proto https

Важный момент: обязательно передавай заголовки X-Forwarded-For и X-Real-IP. Без них приложение видит только IP балансировщика вместо реального клиента — это ломает логи, геоаналитику и защиту от ботов.

Sticky Sessions — когда round-robin не подходит

Если приложение хранит сессии локально на сервере (не в Redis, не в БД), нужно чтобы пользователь всегда попадал на тот же сервер.

В Nginx — ip_hash. Проблема: при мобильных сетях или CDN много пользователей могут быть за одним IP, нагрузка распределяется неравномерно.

В HAProxy — cookie-based sticky sessions, это надёжнее:

backend http_back
    balance roundrobin
    cookie SERVERID insert indirect nocache
    server web1 10.0.0.1:8080 check cookie web1
    server web2 10.0.0.2:8080 check cookie web2

HAProxy добавляет cookie SERVERID в ответ. При следующем запросе читает cookie и направляет на тот же сервер.

Но лучше вообще убрать локальные сессии — вынеси их в Redis, и проблема исчезнет. Тогда любой алгоритм балансировки работает корректно.

Zero-downtime деплой

Это одна из главных причин, зачем вообще нужна балансировка. Обновляешь серверы по очереди:

  1. Убираешь web1 из балансировки
  2. Деплоишь новую версию на web1
  3. Проверяешь что всё работает
  4. Возвращаешь web1 в пул
  5. Повторяешь для web2

Пользователи работают без перебоев.

HAProxy умеет делать это через сокет без перезагрузки всего процесса:

echo "disable server http_back/web1" | socat stdio /run/haproxy/admin.sock
echo "enable server http_back/web1" | socat stdio /run/haproxy/admin.sock

Типичные ошибки при настройке

Забытые таймауты. Без них соединения будут висеть вечно при зависших серверах. timeout server 30s — разумное значение для большинства приложений.

Нет health check. Без проверки балансировщик продолжает слать запросы на мёртвый сервер. Всегда настраивай.

Нет X-Forwarded-For. Приложение логирует IP балансировщика вместо реального клиента — не видишь кто что делает, сложнее разбирать инциденты.

Одинаковый вес при разных машинах. Если серверы разные по мощности — ставь веса соответственно, иначе слабый сервер захлебнётся.

Хранение сессий локально. Настроил round-robin, пользователи случайно разлогиниваются — потому что попадают на разные серверы. Выноси сессии в Redis.

Как это выглядит в реальном проекте

Команда REEXY настраивала подобную инфраструктуру при запуске нагруженных интернет-магазинов и корпоративных порталов. Типичная схема для среднего проекта:

Клиент → Nginx (SSL, статика, кэш) → HAProxy → [App1, App2, App3] → PostgreSQL

Nginx на входе — потому что умеет быстро отдавать статику и кэшировать ответы. HAProxy за ним — потому что лучше балансирует динамические запросы и даёт детальную статистику по каждому backend-серверу.

Для серьёзных проектов ставят два балансировщика с Keepalived — виртуальный IP переходит на резервный если основной упал. Тогда и сам балансировщик не является single point of failure.

Когда что выбирать — коротко

Бери Nginx upstream если:

  • Небольшой проект, 2–3 сервера
  • Уже используешь Nginx как веб-сервер
  • Не нужна сложная маршрутизация

Бери HAProxy если:

  • Высокие требования к отказоустойчивости
  • Нужна детальная маршрутизация по URL, заголовкам, cookie
  • Важна подробная статистика
  • Большое количество серверов в пуле
  • Нужна TCP-балансировка (не только HTTP)

Оба варианта production-ready и используются в крупных проектах. Разница — в деталях архитектуры и в том, насколько сложными будут твои требования к маршрутизации и мониторингу.