Балансировка нагрузки: как раскидать запросы по серверам и не потерять ни одного

Пятая статья серии «Архитектура и производительность». Ранее мы разобрали, когда код надо делить на сервисы (монолит против микросервисов), как они общаются (проектирование API) и как ускорять ответы (кеширование). Теперь посмотрим на систему со стороны сети: у вас есть адрес, по которому приходят пользователи, и группа серверов, которые выполняют работу. Кто-то должен решать, какой именно сервер получит каждый новый запрос. Этот «кто-то» — балансировщик нагрузки. Звучит как деталь больших дата-центров, но нужен даже проекту с двумя серверами. Разберём: зачем он, чем четвёртый уровень отличается от седьмого, какие бывают алгоритмы распределения, как балансер понимает, что сервер умер, и чем опасны «липкие сессии». Плюс готовые фрагменты конфигураций Nginx и HAProxy.


Часть I. Зачем нужен балансировщик: нагрузка и единая точка отказа

1. Проблема одного сервера

Магазин живёт на одном VPS: там и Nginx, и PHP-FPM, и MySQL. Хватает на блог или на магазин с парой тысяч посетителей в день. Но у схемы два недостатка, и оба вылезают, когда проект становится важнее, чем вы ожидали.

Производительность. Один сервер обрабатывает конечное число запросов в секунду: есть предел по CPU, памяти, числу соединений и пропускной способности. Когда магазин попадает в крупную подборку, трафик растёт в десять раз за час, страницы открываются за секунды, потом браузер показывает пустоту. Никакой настройкой одного сервера вы не получите больше, чем у него восемь ядер.

Надёжность. Если сервер один, его поломка — полная недоступность продукта. Упал PHP-FPM — сайт лежит. Кончилось место на диске — сайт лежит. Пользователь не различает «проблема на половине путей» и «нас нет вообще».

Здесь появляется балансировщик нагрузки — промежуточный сервер, который принимает все входящие запросы и сам решает, какому из внутренних передать каждый. Пользователь видит только адрес балансера. Внутри — пул бэкенд-серверов (backend servers, «бэкенды»), их называют также апстримами (upstream — так их зовёт Nginx).

браузеры --HTTP--> [ балансировщик ] --> бэкенд-1 (PHP-FPM)
                                      --> бэкенд-2 (PHP-FPM)
                                      --> бэкенд-3 (PHP-FPM)

При поломке любого бэкенда запросы идут на остальные: пользователь заметит разве что задержку в сотню миллисекунд, но сайт не исчезнет.

2. Единая точка отказа простыми словами

Единая точка отказа (single point of failure, SPOF) — элемент архитектуры, выход которого из строя останавливает всю систему. Каждая точка проверяется вопросом: «если это умрёт — всё ляжет?».

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

Что у вас естьТочка отказаСтоимость
Один серверОн самминимально
Балансер + бэкендыБалансернемного
Балансер + база на одном хостеБазанемного
Пара балансеров, бэкенды, реплика базыПровайдер, DNSзаметно
Два дата-центра в разных регионахАвария регионадорого

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

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

3. Что балансер умеет, кроме пересылки

  • Исключение умершего сервера из пула и возврат его, когда он снова начнёт отвечать.
  • Завершение TLS (termination): балансер принимает HTTPS, расшифровывает и отправляет на бэкенды обычный HTTP. Сертификат лежит в одном месте, процессоры бэкендов не тратятся на криптографию.
  • Маршрутизация по содержимому: /api/ на микросервисы, статика на файловый сервер, админка — только из внутренней сети.
  • Переиспользование соединений к бэкендам вместо открытия нового на каждый запрос.
  • Сбор метрик: сколько запросов, какая задержка, сколько ошибок (статья Метрики и мониторинг).

Не путайте балансировщик с CDN (content delivery network, сеть доставки контента): CDN раздаёт статику из точек присутствия и не вызывает ваш код, а балансер работает с живыми серверами и динамическими ответами. Они дополняют друг друга: CDN отсекает пиковый трафик на картинках, балансер распределяет то, что дошло до исходного сервера (статья CDN и edge-вычисления).


Часть II. Уровень 4 против уровня 7: куда смотрит балансер

4. Два взгляда на один запрос

Модель OSI (семь уровней сети) нужна только для одного: понять слова «четвёртый уровень» и «седьмой уровень».

  • Уровень 4 — транспортный, здесь живут TCP и UDP. Полезная информация: IP-адрес отправителя, IP-адрес получателя, номер порта. Запроса, URL и заголовков тут ещё нет — есть поток байтов в соединении.
  • Уровень 7 — прикладной, это HTTP: метод (GET, POST), путь (/api/orders/123), заголовки (Authorization, Cookie), тело запроса.

Балансировщик четвёртого уровня работает с соединением целиком: не разбирает HTTP, увидел пакет с адресами и портом — передал в нужное место. Клиент и бэкенд дальше общаются напрямую.

Балансировщик седьмого уровня заканчивает соединение сам: принимает HTTP-запрос клиента целиком, разбирает, решает, кому отправить, открывает отдельное соединение с бэкендом и передаёт запрос туда. Клиентское и серверное соединение — два независимых куска.

ПризнакL4L7
Что видитIP-адреса, портыURL, заголовки, куки, тело
Скоростьвыше: не разбирает HTTPниже: парсит каждый запрос
Маршрутизация по /api/нетда
Работа с TLSпробрасываетрасшифровывает на себе
Health check«порт открыт»«страница /ready вернула 200»

L4 имеет смысл, когда трафик очень большой и важнее скорость; когда протокол вообще не HTTP (сырой сокет, база данных); когда бэкенды сами разбирают TLS. Примеры: LVS, IPVS, облачный балансер в сетевом режиме, HAProxy в режиме tcp.

L7 нужен почти всегда в веб-разработке: отправить /api/ на PHP-FPM, а /images/ на раздачу файлов; прочитать Host, чтобы обслуживать несколько доменов с одного балансера; проверять живость не «порт слушает», а «страница отвечает 200»; корректно работать с WebSocket и HTTP/2. Классические реализации — Nginx и HAProxy (оба в режиме http), реже Traefik, Envoy, AWS ALB.

5. Где физически живёт балансер

Облачный управляемый (ALB в AWS, «Балансировщик нагрузок» в Yandex Cloud): платите за час, инфраструктуру обслуживает провайдер, метрики уже есть, внутри облака он сам дублирован. Минус — привязка к облаку и цена при большом трафике. Свой на отдельном VPS: полный контроль, дёшево, прозрачно; минус — вы сами отвечаете за обновления и за свою единственную точку отказа. Внутри того же Nginx, что раздаёт сайт — самый частый вариант для небольшой команды: тот же L7, только не выделенный в отдельный сервис; зато раздатчик статики и балансировщик становятся одним процессом, и его поломка валит всё.

DNS-балансировка (домен отдаёт несколько A-записей) настоящей балансировки не даёт: браузер не знает, жив ли сервер, а кеш DNS держит ответ часами. Как способ развести по геологии — годится, как замена балансировщику — нет.


Часть III. Алгоритмы распределения: round-robin, least-connections, IP-hash

Алгоритм (метод балансировки) — правило, по которому балансер выбирает бэкенд для следующего запроса.

6. Round-robin: по кругу

Самый простой метод: указатель сдвигается на каждый запрос — первый серверу 1, второй серверу 2, третий серверу 3, четвёртый снова серверу 1. В Nginx это метод по умолчанию, отдельной директивы не надо. weight (вес) задаёт долю запросов, если серверы разные по мощности:

upstream backend_rr {
    # round-robin по умолчанию; weight — доля запросов на сервер
    server 10.0.0.11:9000 weight=3;   # новая машина: 3 запроса из 5
    server 10.0.0.12:9000 weight=2;   # старая: 2 запроса из 5
    server 10.0.0.13:9000;
}

Когда хорош: запросы примерно одинаковые по стоимости, серверы примерно одинаковой мощности. Где ломается: round-robin не знает, сколько запрос будет выполняться, — он считает «переданную единицу», а не «нагрузку». Если один сервер из трёх отдаёт выгрузку отчёта за пять секунд, а остальные — страницы за пятьдесят миллисекунд, через минуту отчётный сервер задохнётся, а остальные будут простаивать.

7. Least-connections: у кого меньше активных соединений

Более честный метод. Балансер держит счётчик активных соединений для каждого бэкенда и отправляет запрос туда, где счётчик меньше; при равенстве выбор идёт по round-robin. В HAProxy это balance leastconn.

upstream backend_lc {
    least_conn;
    server 10.0.0.11:9000;
    server 10.0.0.12:9000;
    server 10.0.0.13:9000;
}

Когда хорош: время ответа различается от запроса к запросу. Метод сам «наказывает» медленный сервер: чем дольше он держит соединения, тем меньше новых запросов получает. Где ломается: при разной стоимости соединения счётчик вводит в заблуждение — скачивание файла засчитывается как короткий запрос. А если все ответы быстрые, метод просто дороже round-robin по процессору.

8. IP-hash и прочие методы

Вычисляется хеш от IP-адреса клиента: один и тот же адрес всегда попадает на один и тот же бэкенд. consistent включает консистентное хеширование — при добавлении или удалении сервера перераспределяется только малая часть клиентов, а не весь пул.

upstream backend_hash {
    hash $remote_addr consistent;
    server 10.0.0.11:9000;
    server 10.0.0.12:9000;
    server 10.0.0.13:9000;
}

Когда хорош: нужно привязать клиента к серверу без куки либо важен локальный кеш на бэкенде. Где ломается: адрес клиента — плохая единица. NAT (общий адрес на весь офис или на мобильного оператора) превращает «привязку клиента» в «привязку тысяч людей к одному серверу».

Что ещё бывает: случайный выбор (balance random) прост и при большом числе запросов даёт ровную картину; наименьшая задержка берёт сервер, быстрее всех ответивший в последнем замере (нужна активная проверка живости); привязка по пути полезна для локального кеша. Практический выбор: начинайте с round-robin, переходите на least-connections, когда запросы стали различаться по длительности, а IP-hash берите только осознанно.


Часть IV. Health checks: как балансер узнаёт, что сервер умер

9. Пассивные и активные проверки

Если балансер не проверяет бэкенды, он честно отправит запрос на сервер, который лежит и молчит, — а этого мы и хотели избежать.

Пассивные проверки. Балансер ничего не проверяет заранее, только считает реальные ошибки при обслуживании запросов: не удалось соединиться, сервер вернул таймаут или 5xx — неудача. После N подряд неудач бэкенд исключается на время fail_timeout.

upstream backend_passive {
    least_conn;
    server 10.0.0.11:9000 max_fails=3 fail_timeout=30s;
    server 10.0.0.12:9000 max_fails=3 fail_timeout=30s;
    server 10.0.0.13:9000 max_fails=2 fail_timeout=60s backup;
}

Расшифровка построчно: max_fails=3 — порог срабатывания, три неудачи за окно fail_timeout, и сервер объявляется мёртвым; fail_timeout=30s — окно подсчёта неудач и одновременно время изоляции до следующей попытки; backup — резерв, используется только когда все остальные исключены. Плюс пассивных проверок — ничего настраивать на бэкенде не надо. Минус — часть пользователей всегда успевает получить ошибку: три неудачи — это три упавших запроса.

Активные проверки. Балансер сам, по таймеру, ходит на специальный адрес каждого бэкенда: «ты жив?». Раз в interval секунд открывает соединение с http://бэкенд/ready и ждёт ответа; если код не тот, что ожидается, — неудача. После fails неудач подряд сервер исключается, после passes успешных — возвращается. Пользовательские запросы при этом не приносятся в жертву.

10. Что должен возвращать health-check эндпоинт

Главная ошибка — сделать /health, который просто возвращает 200. Такой ответ означает лишь «процесс жив», а процесс может быть жив и бесполезен: воркеры PHP зависли, пул соединений к базе исчерпан, место на диске кончилось. Обычно делают два эндпоинта: /health (liveness, «жив ли») — быстрый, по нему оркестратор перезапускает процесс; /ready (readiness, «готов ли принимать трафик») — проверяет базу, кеш и диск, и именно его должен опрашивать балансер. Правило: пока всё в порядке — HTTP 200, как только критичная проверка провалилась — HTTP 503 (Service Unavailable, «сервис временно недоступен»).

{
  "status": "ok",
  "checks": {
    "database":    { "status": "ok", "latency_ms": 3 },
    "redis":       { "status": "ok", "latency_ms": 1 },
    "disk_free":   { "status": "ok", "value": "124 GB" },
    "queue_depth": { "status": "ok", "value": 42 }
  },
  "uptime_seconds": 3621,
  "version": "1.7.3"
}

На PHP такой эндпоинт готовности выглядит примерно так:

PHP
<?php
// health.php — ответ для активных проверок балансировщика
header('Content-Type: application/json; charset=utf-8');
$checks = [];
$ok = true;

try {                                   // 1. База: простой запрос без чтения данных
    $pdo->query('SELECT 1')->fetchColumn();
    $checks['database'] = ['status' => 'ok'];
} catch (Throwable $e) {
    $checks['database'] = ['status' => 'fail'];
    $ok = false;
}

$redisOk = $redis->ping();              // 2. Доступен ли кеш
$checks['redis'] = ['status' => $redisOk ? 'ok' : 'fail'];
$ok = $ok && $redisOk;

$limit = 500 * 1024 * 1024;             // 3. Хватает ли места на диске
$free = disk_free_space('/var/lib/php/sessions');
$checks['disk_free'] = ['status' => $free > $limit ? 'ok' : 'fail'];
$ok = $ok && $free > $limit;

http_response_code($ok ? 200 : 503);
echo json_encode(['status' => $ok ? 'ok' : 'degraded', 'checks' => $checks],
    JSON_UNESCAPED_UNICODE);

11. Пороги срабатывания в обе стороны

В Nginx активные проверки описываются внутри upstream (синтаксис различается в открытом и коммерческом сборках; в открытом добавляют модуль nginx_upstream_check_module):

upstream backend_active {
    least_conn;
    server 10.0.0.11:9000;
    server 10.0.0.12:9000;
    server 10.0.0.13:9000;

    health_check interval=5s fails=3 passes=2 uri=/ready;
}

interval=5s — частота опроса, fails=3 — три неудачных ответа подряд исключают сервер, passes=2 — два удачных возвращают его в пул. Возвращать после одной удачи — плохая идея: процесс мог подняться и ещё не прогреть кеш, поэтому возвращают после нескольких удачных проверок, иногда с slow_start — временем постепенного роста доли трафика. Слишком агрессивные пороги тоже вредны: fails=1 при interval=1s означает, что заминка на долю секунды уберёт здоровый сервер из пула и обрушит его трафик на соседей — так начинается каскадный отказ. И помните: health-check должен быть дешёвым, иначе он сам создаёт нагрузку в число_серверов × (1 / interval) запросов в секунду.


Часть V. Липкие сессии: когда они нужны и когда приносят вред

12. В чём проблема

HTTP по природе не помнит клиента. Чтобы пользователь оставался «залогиненным», сервер ставит куку с идентификатором сессии, а данные сессии хранит у себя. Беда начинается, когда эти данные лежат в памяти конкретного процесса PHP на конкретном сервере: клиент зашёл на бэкенд-1, сессия создалась в его памяти, через секунду тот же запрос ушёл на бэкенд-2, где про сессию никто не знает, — и пользователя выбросило на страницу логина.

Решений ровно два. Первое, правильное — вынести состояние сессий в общее хранилище: Redis или базу данных. Тогда любой бэкенд читает любую сессию, липкость не нужна (альтернатива без внешнего хранилища — JWT-токен, который не требует хранения на сервере, см. Безопасную аутентификацию). Второе — заставить балансер привязывать клиента к одному серверу; это и есть sticky sessions, липкие сессии. Привязка идёт через куку, которую ставит сам балансер:

upstream backend_sticky {
    least_conn;
    server 10.0.0.11:9000;
    server 10.0.0.12:9000;
    server 10.0.0.13:9000;

    sticky cookie srv_id expires=1h domain=.example.com path=/;
}

Клиент получил куку srv_id=10.0.0.12 — все последующие запросы уходят на второй сервер. В облачных балансерах тот же механизм называется AWSALB (AWS), GSERVERID (Yandex Cloud), route или affinity (Azure).

13. Когда липкость вредна и когда оправдана

Кажется, что липкость — панацея: и логин работает, и переписывать не надо. На практике это источник трёх проблем. Перекос нагрузки: как только клиент приклеился, алгоритмы распределения для него перестают работать — бэкенд-1 получил десять «приклеенных» клиентов с долгими соединениями, бэкенд-2 свободен, но перевести туда старых нельзя. Потеря сессий остаётся: упал бэкенд-1 — его клиенты прилетят на другие серверы, где их сессий нет, и разлогинятся. Мешает обновлению: раскатка релиза по одному серверу (rolling deploy) уносит сессии исключённого сервера, а вновь созданный сервер не получает клиентов, потому что все старые приклеены к прежним адресам.

СитуацияЛипкость
Сессии в памяти PHP, переписать быстро нельзянужна как временная мера
Долгоживущее WebSocket-соединение плюс запросы к одному обработчикунужна (соединение не перенести)
Сессии в Redis, в базе или JWTне нужна
Кеш на бэкенде (OPcache плюс локальные файлы)опциональна, лучше ip-hash
Тяжёлые выгрузки отчётоввредна — нужен least-connections
Тысячи пользователей за одним корпоративным NATвредна — приклеит всех к одному серверу

Правило простое: липкие сессии — костыль, а не архитектура. Они нужны ровно в одном случае: когда не можете вынести состояние наружу прямо сейчас, а сервис должен работать сегодня. План — липкость как временная мера, состояние в Redis как цель.


Часть VI. Конфигурации: Nginx upstream и HAProxy на практике

14. Nginx как балансировщик седьмого уровня

Nginx
upstream app_backend {
    least_conn;                      # метод: у кого меньше активных соединений

    server 10.0.0.11:9000 max_fails=3 fail_timeout=30s;
    server 10.0.0.12:9000 max_fails=3 fail_timeout=30s weight=2;
    server 10.0.0.13:9000 max_fails=3 fail_timeout=30s;
    server 10.0.0.99:9000 backup;    # только когда все остальные исключены

    keepalive 32;                    # держим соединения к бэкендам открытыми
}

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

    location / {
        proxy_pass http://app_backend;

        # Реальный адрес клиента — иначе все бэкенды видят IP балансера
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Connection "";   # нужно для переиспользования keepalive

        proxy_connect_timeout 2s;
        proxy_read_timeout    60s;

        # Не показывать ошибку сразу — попробовать следующий бэкенд
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 2;
    }

    location ~* \.(jpg|jpeg|png|webp|css|js|woff2)$ {  # статика — минуя PHP
        root /var/www/shop/public;
        expires 30d;
    }
}

Что из-за чего ломается:

  • Host без него бэкенд получает имя app_backend вместо shop.example.com, приложение строит адреса исходя из него, и пользователя уносит редиректом на внутреннее имя.
  • X-Real-IP и X-Forwarded-For — без них логи, антифрод и лимиты видят IP балансера вместо IP посетителя; на стороне PHP читайте X-Forwarded-For аккуратно — ту запись, которую добавили вы, а не всю цепочку от клиента (её подделывают).
  • proxy_next_upstream говорит: при 502, 503, 504 или обрыве соединения не показывать ошибку, а повторить запрос на следующем бэкенде. Опасность — повторное выполнение: GET повторять можно, а POST заказа тоже, и пользователь получит два заказа; non_idempotent включайте только осознанно.
  • keepalive экономит рукопожатие на каждом запросе; при большом QPS (queries per second, запросов в секунду) заметно.

15. HAProxy: минимальный конфиг бэкенда

Файл haproxy.cfg устроен из секций: global (ресурсы процесса), defaults (настройки по умолчанию), frontend (что слушаем снаружи), backend (куда отправляем внутрь).

global
    maxconn 20000              # потолок одновременных соединений на процесс
    log     /dev/log local0

defaults
    mode    http               # L7; для L4 здесь было бы mode tcp
    option  httplog            # подробный лог каждого HTTP-запроса
    timeout connect 2s         # сколько ждать установки соединения с бэкендом
    timeout server  60s        # сколько ждать ответ от бэкенда
    retries     2              # сколько раз попробовать другой сервер

frontend https_in
    bind *:443 ssl crt /etc/haproxy/certs/
    default_backend app_backend

backend app_backend
    balance leastconn          # варианты: roundrobin | leastconn | source | uri

    # активная проверка живости: просим /ready и ждём код 200
    option httpchk GET /ready HTTP/1.0
    http-check expect status 200

    # пороги: интервал опроса, 3 неудачи — исключение, 2 успеха — возврат
    default-server check inter 5s fall 3 rise 2 slowstart 30s

    # привязка по куке, которую ставит сам HAProxy
    cookie SRVID insert indirect nocache
    server app1 10.0.0.11:8080 cookie a1 check
    server app2 10.0.0.12:8080 cookie a2 check weight 2
    server app3 10.0.0.13:8080 cookie a3 check
    server app4 10.0.0.99:8080 cookie a4 check backup

Ключевые слова: mode http включает L7-режим (в mode tcp HAProxy не смотрит в HTTP и становится быстрым L4-балансером); option httpchk — активная проверка живости по таймеру для каждого сервера с check; http-check expect status 200 — критерий успеха; inter 5s fall 3 rise 2 — интервал опроса и пороги исключения/возврата; slowstart — плавный рост доли трафика для непрогретого процесса; backup — резерв на случай, когда все остальные исключены; cookie ... insert indirect nocache — привязка по собственной куке (indirect — не показывать куку приложению, nocache — не кешировать ответ из-за неё). Есть и listen stats — встроенная панель, где видно, какой бэкенд исключён и почему; доступ к ней держат из внутренней сети.

16. Как проверить, что балансировка работает

  • Запросите /ready на каждом бэкенде и через балансер — ответы должны совпасть. Если на бэкенде 200, а через балансер 503, ошиблись в критерии.
  • Выключите один бэкенд (systemctl stop php-fpm) под нагрузкой: правильно — несколько ошибок на переходе и затем ноль; если ошибки длятся, проверки живости нет или пороги медленные.
  • Верните бэкенд: часто rise завышен, и сервер стоит без трафика пять минут.
  • Посчитайте распределение: тысяча запросов, деление по логам. Для round-robin — примерно равное, для weight=2 — двойная доля; втрое больше на одном сервере — значит, осталась липкость или IP-hash при NAT.
  • Проверьте под реальной нагрузкой — методика в статье Нагрузочное тестирование.

Часть VII. Типичные ошибки и чек-лист

17. Что чаще всего ломают

  • Один балансер перед одним сервером. Формальность, добавляющая задержку и точку отказа: балансировка имеет смысл, только когда бэкендов минимум два.
  • Балансировка базы данных тем же способом. MySQL и PostgreSQL нельзя «распределить» round-robin: это не сервисы без состояния (stateless — «не помнящий состояния»). Базу решают репликацией и пулом соединений.
  • Health check, который всегда 200, и слишком агрессивные пороги: в первом случае балансер думает, что всё в порядке, пока приложение отдаёт пустые страницы, во втором — заминка в одну секунду выгоняет живой сервер и запускает каскадный отказ.
  • Липкие сессии при сессиях в Redis. Состояние уже в общем хранилище, а липкость осталась из прошлого конфига и зря перекашивает нагрузку.
  • Отсутствие X-Forwarded-For и X-Forwarded-Proto. Приложение видит IP балансира: лимиты считаются с одного адреса, антифрод банит всех сразу, а без заголовка о протоколе HTTPS-клиент попадает в цикл редиректов.
  • Не выставленные таймауты. Зависший бэкенд держит воркера минуту (столько стоит proxy_read_timeout по умолчанию), и соединения быстро кончаются.
  • Не проверено поведение при падении. Конфиг красив, а в реальном отказе выясняется, что резерв не настроен, а повторы добивают базу.

18. Чек-лист перед включением

  • Бэкендов минимум два, они равноценны либо различие описано весами weight.
  • Алгоритм выбран осознанно: round-robin для одинаковых запросов, least_conn для разнородных, ip-hash — с пониманием проблемы NAT.
  • Health check активный, смотрит на эндпоинт готовности и проверяет базу, кеш и диск; пороги разумные (5–10 секунд, 3 неудачи, 2 успеха) с медленным стартом после возвращения.
  • Липкость либо выключена, либо документирована как временная мера с планом отказа от неё.
  • X-Real-IP, X-Forwarded-For, X-Forwarded-Proto передаются; приложение доверяет им через список доверенных прокси.
  • Таймауты выставлены явно, proxy_next_upstream не применяется к запросам, которые нельзя повторять.
  • Резервный сервер (backup) есть, и его ёмкости хватает, чтобы принять весь трафик в одиночку.
  • Метрики с балансира идут в мониторинг (статья Метрики и мониторинг), конфиг лежит в версиях контроля, а поведение проверено выключением сервера под нагрузкой.
  • Сам балансер не единственная точка отказа: облачный управляемый, пара своих с плавающим адресом (keepalived, VRRP) — либо осознанное согласие с риском.

Итог. Что даёт балансировка и чего она не даёт

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

Дальше всё сводится к трём вопросам. Где балансируете: L4 — быстро, по адресам и портам, не знает про HTTP; L7 — читает запрос, маршрутизирует по пути и заголовкам, и именно это нужно веб-приложению. Как выбираете сервер: round-robin для ровных запросов, least-connections для разнородных, IP-hash только когда понимаете цену. Как узнаёте о смерти сервера: пассивно — по ошибкам пользователей, активно — по дешёвым проверкам эндпоинта готовности с разумными порогами. Липкие сессии — не архитектура, а временный мостик для приложения, которое хранит состояние в памяти; настоящий путь — вынести состояние в общее хранилище. Чего балансировка не даёт: она не делает быстрым медленный код, не заменяет кеширование и не спасает от упавшей базы.

Материал написан простым языком. Примеры конфигураций рассчитаны на размещение на VPS (WordPress и собственные PHP-приложения) и используют типичный набор: Nginx, HAProxy, PHP-FPM, Redis. Синтаксис конкретных версий стоит сверять с официальной документацией: директивы активных проверок живости в Nginx различаются в открытом и коммерческом сборках, а в HAProxy часть опций переезжала между секциями в линейке 2.x.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.