В этом уроке разберём механику nginx балансировки до самого дна: как устроен пул бэкендов, какие методы распределения бывают и когда какой брать, что реально делают max_fails и backup, почему keepalive к апстриму - это не опция, а необходимость, и что важного поменялось в nginx 1.30 в 2026 году (спойлер: дефолты стали умнее, а sticky-сессии наконец-то приехали в open-source). Опираться будем на боевой upstream.conf из эталонного gateway-проекта.
Что такое nginx upstream и как он включается в поток запроса
Директива upstream объявляет именованную группу серверов - пул бэкендов. Сам по себе блок ничего не проксирует, он только описывает, куда и как раскидывать. Подключается он по имени в proxy_pass (или fastcgi_pass, uwsgi_pass и т.д.).
Код: Выделить всё
upstream backend_upstream {
server 10.0.0.1:9000 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.0.2:9000 max_fails=3 fail_timeout=30s;
server 10.0.0.3:9000 max_fails=3 fail_timeout=30s;
server 10.0.0.4:9000 backup;
keepalive 64;
}
server {
location ^~ /api/ {
proxy_pass http://backend_upstream;
}
}
Важно держать в голове область видимости: upstream живёт в блоке http, а выбор сервера происходит на каждый запрос. Пул соединений (keepalive) и счётчики ошибок - per-worker, то есть каждый рабочий процесс ведёт свой учёт. Это объясняет часть неинтуитивного поведения, к которому вернёмся в граблях.

Методы балансировки: round-robin, least_conn, ip_hash, hash, random
Метод задаётся одной директивой внутри upstream-блока. По умолчанию - взвешенный round-robin (директиву писать не нужно). Разберём арсенал по делу.
Round-robin (по умолчанию). Запросы идут по кругу пропорционально весам. weight=5 у первого сервера в эталоне означает, что он получит примерно в 5 раз больше запросов, чем узел с весом по умолчанию (1). Идеален, когда все запросы примерно одинаковы по стоимости и бэкенды stateless. Это базовая nginx load balancing-стратегия, с которой стоит начинать.
least_conn - по наименьшему числу соединений. nginx least_conn отправляет запрос на сервер, у которого сейчас меньше всего активных соединений (с поправкой на вес). Берёшь его, когда время обработки запросов сильно скачет: долгие отчёты, выгрузки, стриминг. Round-robin при таком профиле может завалить один узел длинными запросами, пока соседний простаивает, а least_conn выравнивает реальную загрузку.
Код: Выделить всё
upstream backend_upstream {
least_conn;
server 10.0.0.1:9000;
server 10.0.0.2:9000;
keepalive 64;
}
Код: Выделить всё
upstream backend_upstream {
ip_hash;
server 10.0.0.1:9000;
server 10.0.0.2:9000;
}
Код: Выделить всё
upstream backend_upstream {
hash $cookie_session_id consistent;
server 10.0.0.1:9000;
server 10.0.0.2:9000;
}
Практическое правило: stateless и равномерные запросы - round-robin; разброс по времени обработки - least_conn; нужна липкость без общего стораджа - hash по cookie с consistent (а не ip_hash, если за прокси/NAT много клиентов).
Параметры серверов: weight, max_fails, fail_timeout, backup, down, max_conns
Каждая строка server конфигурируется параметрами, и именно тут живёт встроенная отказоустойчивость.
- weight=N - относительный вес в распределении (по умолчанию 1). В эталоне weight=5 у более мощного узла.
- max_fails=N и fail_timeout=T - это пассивный health check. Если за окно fail_timeout накопится N неудачных попыток, сервер на это же время помечается недоступным и выводится из ротации. В эталоне max_fails=3 fail_timeout=30s: три неудачи за 30 секунд - и узел отдыхает 30 секунд, потом nginx снова попробует. Что считается неудачей, задаёт proxy_next_upstream (по умолчанию error и timeout; HTTP 500/502/503/504 туда можно добавить явно).
- backup - запасной сервер, который вступает в игру, только когда все основные недоступны. В эталоне это 10.0.0.4. Удобно держать на нём заглушку-страницу обслуживания или резервный дешёвый инстанс.
- down - помечает сервер как выведенный из эксплуатации вручную (для плановых работ), не удаляя строку из конфига.
- max_conns=N - жёсткий потолок одновременных соединений к узлу. Незаменим, когда бэкенд (например, PHP-FPM с фиксированным pm.max_children) физически не тянет больше N параллельных запросов: лишние подождут в очереди nginx, а не задушат FPM.
keepalive к апстриму: главный рычаг производительности и что изменилось в 2026
Самый частый и самый дорогой промах новичков в nginx балансировке - забыть про keepalive к бэкенду. Без него nginx на каждый клиентский запрос открывает новое TCP-соединение к апстриму, прогоняет три рукопожатия, отдаёт ответ и закрывает сокет. На высоком RPS это выливается в тысячи TIME_WAIT-сокетов, выжранные эфемерные порты и лишние миллисекунды latency на ровном месте.
Директива keepalive N включает пул постоянных соединений: каждый worker держит до N idle-соединений к группе и переиспользует их. В эталоне keepalive 64 - безопасное стартовое значение. Важно понимать: N - это размер кэша idle-соединений на worker, а не лимит общего числа соединений. Под нагрузкой их может быть больше, просто сверх N idle-сокеты закрываются.
Исторически (до 1.30) одного keepalive было мало - нужно было ещё на уровне location явно сказать nginx говорить с бэкендом по HTTP/1.1 и не слать заголовок Connection: close:
Код: Выделить всё
location ^~ /api/ {
proxy_pass http://backend_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Что нового в nginx 1.30 (stable, апрель 2026). Соединение к бэкенду теперь по умолчанию HTTP/1.1 с keepalive - без явных proxy_http_version 1.1 и Connection "". Это снимает классическую ловушку для тех, кто забыл. Но - рекомендация на 2026: если твои конфиги должны работать и на старых версиях (а они почти всегда должны - дистрибутивы тащат разное), всё равно пиши эти две директивы явно. Они валидны, ничего не ломают и страхуют от сюрпризов при разном nginx на разных хостах.
Ещё в 1.30 у keepalive появился новый параметр - keepalive ... local. Он привязывает переиспользуемое соединение к локальному адресу клиентского соединения (полезно, когда исходящий IP к бэкенду важен, например для маршрутизации или ACL на стороне бэкенда). Плюс в 1.30 завезли HTTP/2-соединения к апстриму. Для большинства gateway-сценариев тебе по-прежнему нужен ровно keepalive 64 + (для совместимости) две строки proxy_*.
Sticky-сессии и активные health-чеки: nginx 1.30 против Angie
Раньше за липкость в open-source отвечал только ip_hash (со всеми его проблемами за NAT) или сторонний nginx-sticky-module сомнительной свежести; нормальные cookie-based sticky-сессии были привилегией nginx Plus. В 2026 это изменилось: native sticky приехал в open-source nginx (мейнлайн 1.29.x, вошёл в stable 1.30). Теперь можно прямо в upstream написать:
Код: Выделить всё
upstream backend_upstream {
server 10.0.0.1:9000;
server 10.0.0.2:9000;
sticky name=srv_id expires=1h;
keepalive 64;
}
Грабли липких сессий за NAT (важно для любого варианта). ip_hash ломается, потому что тысячи клиентов за одним внешним IP выглядят как один клиент и едут на один сервер. Cookie-based sticky этого лишён, но имеет свою тонкость: если клиент не принимает cookie (боты, агрессивные приватные режимы, межсервисные вызовы без cookie-jar) - липкость для него не работает, и он раскидывается обычным методом. Поэтому липкость - это оптимизация, а не гарантия: бэкенд должен корректно переживать запрос, прилетевший "не на свой" узел. Лучшее же решение для масштаба - вообще вынести сессии в общий Redis/Memcached и сделать бэкенды stateless, тогда липкость не нужна совсем.
Про Angie. Если тебе нужны именно активные health checks (nginx сам по таймеру опрашивает бэкенды и заранее убирает мёртвые из ротации) и slow_start (плавный набор веса только что поднявшимся узлом, чтобы не завалить его холодным трафиком сразу после старта) - в бесплатном nginx их нет. В Angie они есть из коробки и бесплатно. Выглядит это так:
Код: Выделить всё
# синтаксис Angie
upstream backend_upstream {
server 10.0.0.1:9000 max_conns=200 slow_start=30s;
server 10.0.0.2:9000 max_conns=200 slow_start=30s;
keepalive 64;
}
server {
location ^~ /api/ {
proxy_pass http://backend_upstream;
health_check interval=5s fails=3 passes=2 uri=/healthz;
}
}
Типичные грабли
- Забытый keepalive. На старых nginx (до 1.30) - ещё и забытые proxy_http_version 1.1 + Connection "". Симптом: высокий latency, гора TIME_WAIT в ss -s, исчерпание портов. Лечится тремя строками.
- ip_hash за NAT/CDN. Перекос балансировки в ноль, один узел в огне, остальные простаивают. Лекарство - native sticky (1.30) или общий сторадж сессий.
- Иллюзия активного health check. max_fails - это пассив. Мёртвый бэкенд будет ловить по одному запросу каждые fail_timeout секунд (проверочный заход). Если это больно - Angie или Plus.
- max_fails=0. Отключает пассивную проверку полностью - сервер считается живым всегда. Иногда нужно (когда проверки делает внешний оркестратор), но по незнанию это выстрел в ногу: nginx будет упорно слать на труп.
- Слишком большой keepalive. Завышенный N держит толпу idle-соединений к каждому бэкенду на каждый worker - бэкенд (особенно PHP-FPM) может упереться в лимит коннектов от простаивающих сокетов. Начинай с 64 и смотри метрики.
- Несоответствие weight и реальной мощности. weight=5 на узле, который на деле не в 5 раз мощнее, просто перегрузит его. Веса надо мерить, а не угадывать.
Поднимем два простых бэкенда и раскидаем трафик между ними.
- Шаг 1. Запусти два игрушечных бэкенда, отдающих свой номер. Если есть Docker: (или любые два http-echo/whoami на портах 9001 и 9002).
Код: Выделить всё
docker run -d --name b1 -p 9001:80 -e NGINX_PORT=80 hashicorp/http-echo -text="backend-1" docker run -d --name b2 -p 9002:80 hashicorp/http-echo -text="backend-2" - Шаг 2. Опиши upstream и проксирование:
Код: Выделить всё
upstream lab_upstream { server 127.0.0.1:9001 weight=2 max_fails=3 fail_timeout=10s; server 127.0.0.1:9002 max_fails=3 fail_timeout=10s; keepalive 32; } server { listen 8080; location / { proxy_pass http://lab_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; } } - Шаг 3. Проверь конфиг и перезагрузи:
Код: Выделить всё
nginx -t && nginx -s reload - Шаг 4. Постучись десяток раз и посмотри распределение - backend-1 должен встречаться примерно вдвое чаще из-за weight=2:
Код: Выделить всё
for i in $(seq 1 12); do curl -s http://127.0.0.1:8080/; done | sort | uniq -c - Шаг 5. Убей один бэкенд (docker stop b2) и снова постучись - весь трафик уедет на оставшийся узел, без ошибок в лицо клиенту. Подними обратно (docker start b2) и убедись, что узел вернулся в ротацию после fail_timeout.
- Шаг 6. Поменяй метод: добавь первой строкой в upstream least_conn; затем замени на ip_hash; - перезагружай (nginx -s reload) и наблюдай, как меняется распределение при повторных запросах.
- Чем nginx least_conn отличается от round-robin и при каком профиле нагрузки он выигрывает?
- Почему nginx ip_hash плохо работает, когда много клиентов сидит за одним NAT, и что брать вместо него в nginx 1.30?
- Что конкретно делает связка max_fails=3 fail_timeout=30s и почему это называют пассивным, а не активным health check?
- Зачем нужен keepalive 64 в upstream-блоке и что изменилось в поведении по умолчанию в nginx 1.30?
nginx upstream - это встроенный, бесплатный и быстрый балансировщик: пул серверов, метод распределения (round-robin/least_conn/ip_hash/hash/random two) и пассивная отказоустойчивость через max_fails/fail_timeout/backup. Производительность держится на keepalive к апстриму - в 2026 он по умолчанию в nginx 1.30, но две строки proxy_http_version 1.1 + Connection "" всё равно стоит писать ради совместимости. Native sticky наконец-то в open-source 1.30 и должен вытеснить ip_hash там, где нужна липкость. А если упёрся в потолок пассивных проверок - активные health checks и slow_start ждут тебя бесплатно в Angie. Базовый эталонный шаблон (weight=5, max_fails=3, fail_timeout=30s, backup, keepalive 64) бери как стартовую точку и подкручивай под свои метрики.