Loading...

Производительность · 12 мин чтения

Что такое балансировщик нагрузки? Как трафик распределяется, проверяется и переключается

Балансировщик нагрузки принимает трафик на одном адресе и распределяет его по нескольким серверам, которые все могут на него ответить, пропуская любой сервер, не прошедший проверку состояния. Это руководство разбирает балансировку на уровне 4 и уровне 7, алгоритмы, проверки состояния, sticky sessions и терминацию TLS, рабочие конфигурации HAProxy и nginx, DNS и глобальную балансировку нагрузки, а также ошибки, которые превращают балансировщик в причину аварии.

Обновлено

Что такое балансировщик нагрузки? Как трафик распределяется, проверяется и переключается

Что делает балансировщик нагрузки

Балансировщик нагрузки стоит между клиентами и группой серверов, которые все могут ответить на один и тот же запрос, и решает, какой сервер получит каждый из них. Клиенты подключаются к одному адресу; балансировщик выбирает здоровый backend, передаёт ему запрос и возвращает ответ обратно.

Он выполняет три задачи. Распределяет нагрузку, так что три сервера могут нести примерно втрое больший трафик, чем один. Убирает упавшие серверы: проверки состояния замечают backend, который перестал отвечать, и не отправляют на него никого, пока он не восстановится. И делает изменения невидимыми: можно вывести сервер, обновить его, задеплоить на него и вернуть обратно без ошибок у посетителей.

Вторая задача часто важнее всего: два сервера за балансировщиком — это не столько про ёмкость, сколько про то, что одному из них разрешено упасть. Балансировщик уровня 7 — это разновидность reverse proxy, чья задача — выбирать между одинаковыми backend'ами; HAProxy и nginx делают и то, и другое.

Балансировка на уровне 4 против уровня 7

Уровень — это то, сколько из трафика балансировщик читает перед тем, как принять решение.

Балансировщик уровня 4 работает с TCP-соединениями и UDP-пакетами. Он видит адреса и порты источника и назначения, выбирает backend при открытии соединения и копирует байты в обоих направлениях. Он не может увидеть URL, заголовок или cookie, так что все запросы в этом соединении идут на один и тот же сервер. В обмен он быстрый, не зависит от протокола (базы данных, MQTT, SMTP, игровые серверы) и может пропускать TLS напрямую, так что сертификат остаётся на backend'ах. Здесь работают mode tcp у HAProxy и блок stream {} у nginx.

Балансировщик уровня 7 говорит на HTTP. Он терминирует соединение, читает каждый запрос и может маршрутизировать по хосту, пути, заголовку или cookie: /api в один пул, / в другой, канареечный cookie на новую версию. Он может повторить упавший запрос на другом сервере и распределить запросы одного HTTP/2-соединения по нескольким backend'ам (mode http у HAProxy, http {} у nginx).

Чтобы читать HTTP, балансировщику уровня 7 нужно его расшифровать, так что терминация TLS идёт в комплекте. Сертификат живёт на балансировщике, а backend'ы получают обычный HTTP по приватной сети, либо второе TLS-соединение, если сеть не доверенная. Цена в том, что backend больше не видит клиента: он видит балансировщик. Балансировщики уровня 7 решают это через X-Forwarded-For и X-Forwarded-Proto; балансировщики уровня 4 используют PROXY protocol, который backend должен быть настроен принимать.

Алгоритмы балансировки: round robin, least connections, хеширование, веса

Round robin раздаёт запросы каждому серверу по очереди. Это стандарт почти везде, и он уместен, когда запросы стоят примерно одинаково, а серверы идентичны.

Weighted round robin даёт более крупному серверу бо́льшую долю: с весами 2, 2 и 1 третий сервер получает пятую часть трафика. Веса — это ещё и способ запускать канарейку: добавить новую версию в пул с маленьким весом, понаблюдать за долей ошибок, затем поднять его.

Least connections отправляет следующий запрос серверу с наименьшим числом активных соединений. Используйте его, когда стоимость запроса сильно различается (отчёты рядом с просмотрами страниц, загрузки рядом с вызовами API) или соединения долгоживущие, как у WebSocket.

IP hash (balance source у HAProxy, ip_hash у nginx) привязывает каждый адрес клиента к одному и тому же серверу раз и навсегда. Это даёт affinity без cookie, но распределение зависит от состава клиентов: один офис или один мобильный оператор за общим адресом может посадить тысячи пользователей на один backend.

Consistent hashing по ключу (URI, ID пользователя) закрепляет один и тот же ключ за одним сервером, и при добавлении или удалении сервера переезжает лишь небольшая доля ключей. Это то, что нужно перед кэшами: хешируйте по URI, и каждый объект живёт в одном кэше, а не во всех сразу. nginx записывает это как hash $request_uri consistent;, HAProxy — как balance uri с hash-type consistent.

Какой бы алгоритм вы ни выбрали, он значит меньше, чем проверки состояния, которые говорят правду.

Проверки состояния и failover

Балансировщик нагрузки хорош ровно настолько, насколько верно его представление о том, какие серверы живы.

Активные проверки состояния опрашивают каждый backend по таймеру: открывают TCP-соединение или запрашивают URL вроде /healthz и ожидают 200. После fall подряд неудач сервер помечается как упавший и не получает ничего; после rise успехов он возвращается. HAProxy делает это в open-source версии. Open-source nginx — нет: max_fails и fail_timeout — это пассивные проверки, считающие неудачные реальные запросы и дающие серверу отдохнуть некоторое время. Активные проверки в nginx — часть коммерческого NGINX Plus. Тайминг — это компромисс: проверка каждые две секунды с fall 3 убирает мёртвый сервер за примерно шесть секунд; каждые 30 секунд означают полторы минуты ошибок.

Failover — это то, что происходит дальше. Трафик переходит на оставшиеся серверы, так что у них должен быть запас: три сервера, каждый загруженный на 80%, не смогут поглотить потерю одного. backup-сервер получает трафик только тогда, когда все основные упали, что подходит для холодного резерва или страницы технических работ.

Сделайте так, чтобы health-эндпоинт отвечал на вопрос, который задаёт балансировщик: *должен ли этот сервер получать трафик прямо сейчас?* Это означает проверку того, что локально для процесса (он запустился, может достать свои собственные зависимости, не выключается), и быстрый ответ. Во время деплоя сначала провалите проверку, дайте завершиться уже идущим запросам (connection draining), затем останавливайте процесс.

Привязка сессий (sticky sessions)

Если приложение хранит сессию залогиненного пользователя в памяти одного сервера, следующий запрос должен попасть именно на него, иначе пользователь окажется разлогинен. Привязка сессий (session persistence) заставляет балансировщик помнить, кто куда идёт.

Обычные способы — вставленный cookie (балансировщик добавляет cookie с именем сервера, как это делает HAProxy с cookie SRV insert indirect nocache плюс значение cookie на каждой строке server), cookie приложения, который балансировщик узнаёт (PHPSESSID, JSESSIONID), или хеширование по исходному IP. У open-source nginx есть только методы хеширования (ip_hash или hash $cookie_name); директива sticky принадлежит NGINX Plus.

У привязки есть цена. Нагрузка перестаёт быть равномерной, потому что несколько активных пользователей остаются закреплены за одним сервером. Вывод сервера из эксплуатации занимает столько же времени, сколько длится его самая долгая сессия, а когда сервер падает, его пользователи всё равно теряют сессии.

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

Рабочая конфигурация HAProxy

Конфигурация HAProxy читается сверху вниз: global для процесса, defaults, наследуемый каждой секцией, frontend, принимающий соединения, и backend, хранящий пул.

Пример терминирует TLS на 443 (файл .pem хранит цепочку сертификатов и приватный ключ вместе), перенаправляет обычный HTTP, добавляет заголовки проксирования и балансирует по least connections. Проверка состояния — это настоящий HTTP-запрос с заголовком Host, потому что многие приложения отвечают 404 или редиректом на запрос без него, и проверка, ожидающая 200, тогда падает на здоровом сервере. http-check send требует HAProxy 2.2 или новее; в более старых версиях запрос указывается в строке option httpchk.

default-server задаёт тайминг проверки один раз: опрос каждые две секунды, три неудачи, чтобы пометить сервер упавшим, два успеха, чтобы вернуть его. app3 — машина поменьше и забирает половину доли остальных; spare получает трафик только когда все три упали. Чтобы сделать сессии привязанными, добавьте cookie SRV insert indirect nocache в backend и cookie app1 (и так далее) в каждую строку server.

Проверяйте файл командой haproxy -c -f /etc/haproxy/haproxy.cfg перед каждым reload.

/etc/haproxy/haproxy.cfg: терминация TLS, least connections, HTTP-проверки состояния

global
    log /dev/log local0
    maxconn 20000

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

frontend fe_web
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
    http-request redirect scheme https unless { ssl_fc }
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    default_backend be_app

backend be_app
    balance leastconn
    option httpchk
    http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host app.example.com
    http-check expect status 200
    default-server inter 2s fall 3 rise 2
    server app1 10.0.0.11:8080 check weight 100
    server app2 10.0.0.12:8080 check weight 100
    server app3 10.0.0.13:8080 check weight 50
    server spare 10.0.0.20:8080 check backup

Балансировка нагрузки с nginx upstream

В nginx пул — это блок upstream, и proxy_pass указывает на него по имени. Без строки с алгоритмом nginx использует weighted round robin; least_conn, ip_hash, hash … consistent и random это меняют.

Несколько деталей в примере легко упустить. keepalive 32 держит простаивающие соединения к backend'ам открытыми для повторного использования, но работает только вместе с proxy_http_version 1.1 и пустым заголовком Connection; без этих двух строк nginx открывает новое соединение на каждый запрос. max_fails=3 fail_timeout=10s выводит сервер на десять секунд после трёх неудачных запросов за десять секунд — это пассивная проверка состояния nginx. backup работает только с методами round robin, weighted и least_conn, но не с методами хеширования.

proxy_next_upstream решает, когда упавший запрос повторяется на следующем сервере. По умолчанию nginx повторяет при ошибках соединения и таймаутах, и начиная с версии 1.9.13 никогда не повторяет POST, LOCK или PATCH, если не добавить non_idempotent. Оставьте так: повторять платёж из-за того, что первый сервер завис после списания с карты, хуже, чем показать ошибку. proxy_next_upstream_tries 2 не даёт одному медленному запросу обойти весь пул. Заголовки — те же, что нужны любому reverse proxy; остальное разобрано в руководстве про nginx.

nginx: пул upstream с весами, пассивными проверками, backup и keep-alive

upstream app {
    least_conn;
    server 10.0.0.11:8080 weight=2 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8080 weight=2 max_fails=3 fail_timeout=10s;
    server 10.0.0.13:8080          max_fails=3 fail_timeout=10s;
    server 10.0.0.20:8080 backup;
    keepalive 32;
}

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection        "";
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_connect_timeout 3s;
    }
}

Глобально и локально: DNS, GeoDNS, anycast и облачные балансировщики

Всё, что было выше, — это локальная балансировка нагрузки: один сайт, один пул, прокси на пути запроса. Глобальная балансировка нагрузки решает, какой сайт, регион или дата-центр посетитель достигнет в первую очередь, и обычно это делается до того, как запрос увидит хоть один прокси.

DNS round robin — старейшая форма: опубликовать несколько A-записей, и резолверы раздают их по кругу. Это ничего не стоит и грубо распределяет трафик, но DNS ничего не знает о состоянии. Адрес мёртвого сервера продолжают раздавать, пока кто-то его не убирает, а резолверы и браузеры кэшируют старый ответ на TTL записи и иногда дольше.

GeoDNS отвечает по-разному в зависимости от того, откуда пришёл запрос, так что посетители из одной страны получают адрес ближнего сайта. В сочетании с проверками состояния на стороне DNS это становится DNS failover: регион, не прошедший проверки, убирается из ответов. Ограничения те же — кэширование по TTL и то, что местоположение определяется резолвером, а не посетителем, если резолвер не передаёт часть адреса клиента (EDNS Client Subnet). Руководство про DNS подробно разбирает TTL и резолверы.

Anycast анонсирует один и тот же IP-адрес из многих точек через BGP, и маршрутизация интернета доставляет каждый пакет в ближайшую. Никакого TTL ждать не нужно: когда точка перестаёт анонсироваться, маршруты меняются за секунды-минуты. Так работают крупные DNS-сервисы и CDN, добираясь до своих узлов. Уровни складываются: DNS или anycast выбирает местоположение, прокси внутри него выбирает сервер.

Облачные балансировщики упаковывают те же идеи в управляемый сервис. У AWS есть Application Load Balancer (уровень 7) и Network Load Balancer (уровень 4); Google Cloud Load Balancing предлагает глобальные и региональные, application- и network-варианты; Azure разделяет Azure Load Balancer (уровень 4) и Application Gateway (уровень 7). В Kubernetes Service распределяет соединения по подам, а Ingress-контроллер делает маршрутизацию уровня 7. Алгоритмы, проверки состояния и подводные камни из этого руководства применимы к ним без изменений.

Частые ошибки, вызывающие аварии

Балансировщик становится единой точкой отказа. Три сервера приложения за одной коробкой HAProxy означают, что теперь именно она может всё уронить. Запустите две и переносите плавающий IP между ними через VRRP (обычный инструмент — keepalived), либо поставьте перед ними управляемый или DNS-уровневый слой. Затем проверьте это, выключив активную.

Проверка состояния лжёт, в любую сторону. /healthz, который всегда возвращает 200, держит в ротации сервер с исчерпанным пулом соединений к базе, так что доля запросов падает, а дашборд показывает всё зелёным. Противоположность не лучше: проверка, обращающаяся к общей базе данных, помечает *все* серверы упавшими во время пятисекундного сбоя базы, и короткое замедление превращается в полную аварию. Проверяйте сервер, а не системы, общие для всех серверов.

Несовпадающие таймауты. Если backend закрывает простаивающие keep-alive соединения раньше, чем ожидает балансировщик, балансировщик иногда отправляет запрос в соединение, которое backend только что закрыл, и клиент получает случайный 502. Держите таймаут keep-alive на backend дольше, чем таймаут простоя у балансировщика. Больше причин — в руководстве про 502 Bad Gateway.

Адрес клиента пропадает. Без X-Forwarded-For (уровень 7) или PROXY protocol (уровень 4) приложение логирует, ограничивает по частоте и геолокирует IP балансировщика.

Серверы, которые не идентичны. Разные сборки, разная конфигурация или разные временные метки файлов, меняющие ETag по умолчанию, так что повторная проверка браузера возвращает полный 200 вместо 304, когда отвечает другой сервер. Деплойте один и тот же артефакт везде.

Тестирование простое. Выведите имя backend в заголовке ответа во время тестирования и зациклите запросы; смотрите на распределение, затем остановите один backend и смотрите, как запросы переезжают.

Посмотреть распределение, прочитать состояние сервера, вывести один сервер
# which backend answered? (expose the server name in a debug header first)
for i in $(seq 1 10); do
  curl -s -o /dev/null -D - https://example.com/ | grep -i '^x-served-by'
done

# HAProxy: live state of every server through the runtime socket
# (needs "stats socket /run/haproxy/admin.sock mode 660 level admin" in global)
echo "show servers state be_app" | socat stdio /run/haproxy/admin.sock

# take one server out gracefully before a deploy, then put it back
echo "set server be_app/app1 state drain" | socat stdio /run/haproxy/admin.sock
echo "set server be_app/app1 state ready" | socat stdio /run/haproxy/admin.sock

Где здесь место CDN, и что делает CDN.com.tr

CDN — это глобальная балансировка нагрузки, которую не нужно строить самому. Посетителей маршрутизируют на ближайший edge-сервер, edge-серверы отвечают из кэша всем, чем могут, и только промахи кэша идут к вашему origin-серверу. CDN.com.tr запускает edge-серверы в Турции и за рубежом и распределяет посетителей между ними, так что распределение посетителей по локациям уже сделано до того, как запрос достигнет чего-либо, что контролируете вы.

Вашим остаётся origin-сервер. Для сайта на Pull CDN edge забирает данные с адреса источника, который вы задаёте в панели, — IP или домен. Если вы запускаете несколько origin-серверов, балансировщик, выбирающий между ними, стоит за этим адресом, и всё из этого руководства применимо к нему. Закройте его так, чтобы он принимал соединения только с edge: собственная рекомендация панели — держать адрес origin-сервера вне публичного DNS, чтобы единственный путь к нему шёл через edge.

Edge также смягчает отказы origin-сервера. Контент, уже находящийся в кэше edge, продолжает отдаваться в течение своего TTL, пока origin-сервер лежит, а отчёт Качество трафика помечает копию из кэша, отданную во время медленной работы или падения origin-сервера, как STALE. Тот же отчёт показывает состояние origin-сервера: запросы к origin, долю повторов, ошибки соединения (502/504) и среднее время до первого байта у origin-сервера. Некэшированным и устаревшим объектам всё равно нужен работающий origin-сервер, поэтому ему всё равно нужна собственная избыточность для динамических страниц.

Если вы предпочитаете вообще не держать этот слой самостоятельно, контейнерные приложения на CDN.com.tr принимают количество реплик и health check (путь HTTP, TCP или отсутствие проверки) на каждое приложение. Трафик входит через edge; при нескольких репликах, если один контейнер перезапускается или нездоров, остальные продолжают отдавать, а деплои защищены проверкой состояния, так что выпуск новой версии не открывает окно, в котором приложение недоступно. Сессии идут в управляемый Redis, так что пользователь остаётся залогиненным независимо от того, какая реплика ответит на следующий запрос: рекомендованная выше архитектура без состояния, но без привязывающего cookie.

Частые вопросы про балансировщики нагрузки

HAProxy или nginx для балансировки нагрузки?

Оба быстрые и надёжные. У HAProxy есть активные проверки состояния, привязка через cookie, runtime API для вывода серверов из эксплуатации и очень подробная статистика в бесплатной версии, что делает его более полным именно как балансировщик нагрузки. nginx подходит лучше, когда та же коробка ещё и отдаёт файлы, кэширует или маршрутизирует несколько сайтов, но его бесплатная версия имеет только пассивные проверки состояния.

В чём разница между балансировкой уровня 4 и уровня 7?

Уровень 4 выбирает сервер на каждое TCP-соединение или UDP-поток и никогда не читает содержимое, поэтому работает с любым протоколом и может пропускать TLS без изменений. Уровень 7 читает каждый HTTP-запрос, поэтому может маршрутизировать по URL, заголовку или cookie, повторять запрос на другом сервере и добавлять заголовки проксирования — ценой терминации TLS.

Какой алгоритм балансировки нагрузки выбрать?

Начните с round robin для идентичных серверов и похожих запросов. Переходите на least connections, когда длительность запросов сильно различается или соединения остаются открытыми, как у WebSocket. Используйте consistent hashing, когда один и тот же ключ должен попадать на один и тот же сервер, например URL перед слоем кэша.

Является ли DNS round robin настоящим балансировщиком нагрузки?

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

Нужен ли мне балансировщик нагрузки, если я использую CDN?

CDN распределяет посетителей по собственным edge-серверам и поглощает большую часть трафика из кэша. Если ваш origin-сервер один, балансировщик вам не нужен для ёмкости, хотя origin всё равно остаётся единой точкой отказа для всего некэшируемого. Как только у вас появляется два или больше origin-серверов, поставьте балансировщик за адресом origin-сервера, с которого забирает данные CDN.