Один сервер приложений редко выдерживает нагрузку долго: он становится узким местом при росте трафика, а его падение полностью останавливает сайт. Nginx решает обе задачи — распределяет запросы между несколькими бэкендами и выводит из работы упавший узел. Разберём конфигурацию upstream, алгоритмы, keepalive и диагностику.
Как устроена балансировка в nginx
Директива upstream описывает группу серверов, а proxy_pass отправляет в неё запросы. По умолчанию используется круговой алгоритм (round robin): первый запрос уходит на первый сервер, второй — на второй, и так по кругу. Nginx работает на уровне HTTP и не проверяет, что именно приложение считает успехом: для него ошибкой является сетевой сбой, таймаут или заданный код ответа.
Балансировщик не заменяет горизонтальное масштабирование базы данных и не делает приложение stateless. Если бэкенды хранят сессии в локальной памяти, пользователь будет терять авторизацию при каждом переключении. Это ограничение архитектуры, а не nginx.
Базовая конфигурация upstream
Начнём с простой группы из двух узлов и одного резервного. Параметры max_fails и fail_timeout задают пассивную проверку: после двух неудач за 10 секунд сервер исключается из ротации на 10 секунд.
upstream app_backend {
least_conn;
server 10.0.0.11:8080 weight=3 max_fails=2 fail_timeout=10s;
server 10.0.0.12:8080 weight=1 max_fails=2 fail_timeout=10s;
server 10.0.0.13:8080 backup;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
Вес weight=3 означает, что первый сервер получит втрое больше запросов при равной скорости ответа. Резервный узел backup включается только тогда, когда основные недоступны. Флаг down временно выводит сервер из ротации без удаления строки из конфига — удобно при плановом обслуживании.
Алгоритмы распределения
Круговой алгоритм подходит для одинаковых бэкендов и коротких запросов. Для разнородных задач выбирают другие методы:
least_conn— отправляет запрос туда, где меньше активных соединений. Хорош для долгих запросов и неодинаковой нагрузки.ip_hash— привязывает клиента к серверу по IP. Простое решение для сессий, но ломается за общим NAT и при смене адреса.hash $cookie_session consistent— привязка по cookie с согласованным хешированием. При добавлении узла переезжает минимум ключей.random two least_conn— выбирает два случайных сервера и отправляет запрос менее загруженному. Хорошо масштабируется на десятки узлов.
upstream app_sticky {
hash $cookie_session consistent;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
upstream app_random {
random two least_conn;
server 10.0.0.11:8080;
server 10.0.0.12:8080;
}
Привязка по cookie удобна, но требует, чтобы приложение само устанавливало этот идентификатор. Если его нет, hash посчитает пустую строку и все клиенты попадут на один узел — проверяйте наличие cookie в ответе.
Keepalive и повторные попытки
Без директивы keepalive nginx открывает новое TCP-соединение на каждый запрос, что заметно при высоком RPS. Пул соединений снижает задержку и нагрузку на бэкенды, но работает только вместе с HTTP/1.1 и очисткой заголовка Connection.
upstream app_backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
location /api/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
}
proxy_next_upstream определяет, при каких условиях запрос повторяется на другом сервере. По умолчанию это error timeout. Добавлять http_500 опасно: ошибка приложения может быть детерминированной, и повтор лишь нагрузит второй узел. Для неидемпотентных методов вроде POST повторы по умолчанию отключены — не включайте non_idempotent без понимания последствий.
Диагностика: куда смотреть
Первый шаг — убедиться, что конфигурация валидна, и увидеть итоговый набор директив с учётом всех include. Второй — проверить, как реально распределяются запросы.
sudo nginx -t
sudo nginx -T | sed -n '/upstream app_backend/,/^}/p'
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" http://127.0.0.1/; done
Чтобы понять, какой узел отвечал, добавьте в log_format переменные $upstream_addr, $upstream_status и $upstream_response_time. Тогда в логе будет видно цепочку попыток: два адреса через запятую означают, что первый бэкенд не ответил и запрос ушёл на второй.
log_format upstream '$remote_addr "$request" $status '
'upstream=$upstream_addr '
'status=$upstream_status '
'time=$upstream_response_time';
server {
listen 8081;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
Модуль stub_status показывает активные соединения и счётчики запросов, но не состояние бэкендов. Порты проверяйте через ss -tan state established '( dport = :8080 )', а приложение в обход балансировщика — через curl -H 'Host: example.com' http://10.0.0.11:8080/health.
Типичные ошибки
- Забыли
proxy_set_header Host $host: приложение строит ссылки с внутренним адресом. - Добавили
keepalive, но оставилиConnection: closeот клиента — пул не работает. - Поставили таймаут 60 секунд и ждут минуту на упавшем узле вместо трёх секунд.
- Используют
ip_hashза корпоративным прокси: все сотрудники попадают на один бэкенд.
Чек-лист внедрения
- Проверьте, что все бэкенды отдают одинаковый контент и видят общую базу.
- Задайте health-эндпоинт приложения и таймауты короче клиентских.
- Включите логирование
$upstream_addrдо запуска нагрузки. - Прогоните тест с остановкой одного узла:
systemctl stop appна бэкенде. - Проверьте, что резервный сервер принимает трафик только при отказе основных.
- Опишите процедуру планового вывода узла через
downи перезагрузку nginx.
Итог
Балансировка в nginx строится вокруг нескольких решений: алгоритм распределения, пассивные проверки, пул keepalive и корректные заголовки. Начните с least_conn и коротких таймаутов, добавьте логирование upstream-переменных и только затем усложняйте схему привязкой сессий. Такой порядок позволяет видеть реальную картину и не превращать балансировщик в источник загадочных ошибок.