Почему Nginx - критическая точка отказа и как этого избежать
Nginx обрабатывает входящий трафик и направляет его к бэкендам. Если этот единственный балансировщик выходит из строя, пользователи теряют доступ ко всему сервису. По данным опросов администраторов, до 40% незапланированных простоев веб-приложений связаны с отказом балансировщика нагрузки, а не самих серверов приложений.
Решение состоит из трёх компонентов. Группы upstream объединяют несколько серверов и распределяют нагрузку между ними. Health checks автоматически исключают упавшие узлы без вмешательства администратора. Keepalive-соединения снижают накладные расходы на установку TCP-соединений и ускоряют ответ бэкендов. Эта статья даёт готовые конфигурации для каждого компонента и показывает, как собрать их в единую отказоустойчивую схему.
Если вам нужен более широкий взгляд на балансировку, обратитесь к руководству по алгоритмам и стратегиям распределения трафика в Nginx. Там разобраны методы балансировки, веса и резервные серверы для разных типов приложений.
Группировка бэкендов: настройка upstream для балансировки нагрузки
Директива upstream определяет пул серверов, между которыми Nginx распределяет запросы. Это первый шаг к отказоустойчивости: если один бэкенд недоступен, трафик уходит на остальные. Синтаксис прост, но детали определяют поведение при сбоях.
Базовый пример upstream с двумя бэкендами
Минимальная рабочая конфигурация выглядит так:
upstream backend {
server 192.168.1.10:8080 weight=3;
server 192.168.1.11:8080 weight=1;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
Параметр weight задаёт пропорцию распределения запросов. Сервер 192.168.1.10 получит примерно 75% трафика, сервер 192.168.1.11 - 25%. Веса полезны, когда бэкенды имеют разную аппаратную мощность. Без явного указания веса все серверы считаются равнозначными.
Директива server принимает IP-адрес или доменное имя с портом. Порт обязателен, даже если используется стандартный 80. Nginx не подставляет порт по умолчанию для upstream-серверов.
Выбор метода балансировки под вашу задачу
Метод балансировки определяет, какому бэкенду достанется следующий запрос. Выбор зависит от характера приложения.
Round-robin - метод по умолчанию. Запросы распределяются равномерно по кругу. Подходит для stateless-приложений, где каждый запрос независим. Не учитывает текущую загрузку серверов.
upstream backend {
# round-robin включён по умолчанию
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
Least connections (least_conn) - отправляет запрос серверу с наименьшим количеством активных соединений. Полезен, когда запросы сильно различаются по времени обработки. Длинные запросы не блокируют один сервер, пока другие простаивают.
upstream backend {
least_conn;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
IP hash (ip_hash) - привязывает клиента к конкретному бэкенду по IP-адресу. Все запросы от одного пользователя попадают на один сервер. Критичен для приложений с сессиями на стороне сервера, когда нет общего хранилища сессий.
upstream backend {
ip_hash;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
Random - выбирает сервер случайным образом. С параметром two выбирает два сервера и отдаёт запрос менее загруженному. Компромисс между равномерностью распределения и учётом загрузки.
upstream backend {
random two;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
Детальный разбор алгоритмов с примерами для API, WebSocket и stateful-приложений есть в статье по настройке балансировки Nginx за 5 минут.
Автоматическое исключение упавших серверов: health checks в Nginx
Группа upstream распределяет трафик, но не проверяет доступность серверов. Без health checks запросы продолжают уходить на упавший бэкенд, и клиенты получают ошибки 502 Bad Gateway. Проверки здоровья решают эту проблему автоматически.
Пассивные health checks: настройка в open-source Nginx
Бесплатная версия Nginx использует пассивные проверки. Балансировщик наблюдает за ответами бэкендов и помечает сервер как неработающий после заданного количества неудачных попыток. Настройка выполняется параметрами max_fails и fail_timeout внутри директивы server.
upstream backend {
server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 backup;
}
Параметр max_fails=3 указывает: после трёх неудачных попыток связи с бэкендом в течение fail_timeout=30s сервер считается недоступным на 30 секунд. По истечении этого времени Nginx пробует отправить запрос снова. Если бэкенд отвечает успешно, он возвращается в пул.
Сервер с флагом backup получает трафик только тогда, когда все основные бэкенды помечены как неработающие. Это резервный узел, который держат для критических ситуаций.
Пассивные проверки имеют ограничение: Nginx не инициирует тестовые запросы специально. Он узнаёт о проблеме только когда реальный пользовательский трафик попадает на сбойный сервер. Часть запросов всё равно завершится ошибкой до того, как сервер будет исключён.
Активные health checks в Nginx Plus: преимущества и настройка
Коммерческая версия Nginx Plus отправляет тестовые запросы к бэкендам независимо от пользовательского трафика. Это позволяет обнаружить проблему до того, как она затронет клиентов. Настройка выполняется директивой health_check внутри блока location.
upstream backend {
zone backend 64k;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
health_check interval=5 fails=3 passes=2 uri=/health;
}
}
# Бэкенд должен отвечать на /health
# На стороне приложения добавляется эндпоинт проверки
Директива health_check задаёт интервал проверки (5 секунд), количество последовательных неудач для исключения сервера (3) и количество успешных проверок для возврата в пул (2). URI /health указывает путь, который Nginx запрашивает на каждом бэкенде.
Для проверки не только статуса, но и содержимого ответа используется блок match:
match health_check_response {
status 200;
header Content-Type = application/json;
body ~ "status":"ok";
}
location / {
proxy_pass http://backend;
health_check match=health_check_response;
}
Этот блок требует код 200, заголовок Content-Type равный application/json и наличие строки "status":"ok" в теле ответа. Бэкенд, вернувший код 200, но с неверным содержимым, будет исключён из пула.
Подробное руководство по мониторингу состояний пулов и сбору метрик доступно в статье по настройке health checks и визуализации метрик в Grafana.
Keepalive-соединения: снижаем накладные расходы и ускоряем ответ
По умолчанию Nginx использует HTTP/1.0 для соединений с бэкендами и закрывает TCP-соединение после каждого запроса. Установка нового соединения требует трёхстороннего рукопожатия TCP, что добавляет задержку и нагружает ядро операционной системы. При 1000 запросов в секунду это сотни тысяч лишних системных вызовов в минуту.
Keepalive-соединения переиспользуют установленные TCP-соединения для нескольких запросов. Задержка снижается, пропускная способность растёт.
Настройка keepalive в upstream: пошаговый пример
Для включения keepalive нужно изменить три параметра в конфигурации:
upstream backend {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Директива keepalive 32 в блоке upstream указывает максимальное количество простаивающих keepalive-соединений к каждому бэкенду, которые Nginx держит открытыми. Это не лимит на общее количество соединений, а размер пула для переиспользования.
Строка proxy_http_version 1.1 обязательна. HTTP/1.1 включает keepalive по умолчанию, тогда как HTTP/1.0 требует явного заголовка Connection: keep-alive.
Строка proxy_set_header Connection "" очищает заголовок Connection, пришедший от клиента. Если клиент отправил Connection: close, а Nginx пробросит его бэкенду, соединение закроется после первого же запроса и keepalive не сработает.
Мониторинг и тюнинг keepalive-соединений
Эффективность keepalive оценивается через статистику соединений. Модуль stub_status показывает общее количество принятых, обработанных и текущих соединений, но для детального анализа keepalive с бэкендами нужен модуль ngx_http_upstream_module или внешний мониторинг.
Ориентиры для выбора значения keepalive:
- 32 - достаточное значение для большинства проектов с нагрузкой до 500 запросов в секунду;
- 64-128 - для высоконагруженных систем с тысячами запросов в секунду;
- 256 и выше - для кластеров с десятками бэкендов и очень высоким трафиком.
Каждое keepalive-соединение потребляет память ядра на сокет и буферы. При 128 соединениях к 10 бэкендам это 1280 открытых сокетов, что заметно, но некритично для современного сервера. Проблемы начинаются при тысячах keepalive-соединений на сервер - в этом случае лучше увеличить количество бэкендов, а не пул keepalive.
Симптом нехватки keepalive-соединений: в логах бэкендов появляются записи о частом открытии и закрытии соединений, а время ответа растёт под нагрузкой. Симптом избытка: Nginx потребляет неожиданно много памяти, а команда ss -s показывает десятки тысяч TCP-сокетов в состоянии ESTABLISHED.
Для комплексного мониторинга производительности Nginx используйте шпаргалку по пяти ключевым метрикам: Active Connections, Requests per Second, ошибки 4xx/5xx, Upstream Response Time и Waiting Connections.
Комплексный пример: собираем всё вместе для отказоустойчивого кластера
Конфигурация ниже объединяет upstream с двумя бэкендами, пассивные health checks, резервный сервер и keepalive-соединения. Этот файл готов к использованию после замены IP-адресов на реальные.
upstream backend {
# Метод балансировки - least_conn для неравномерных запросов
least_conn;
# Основные бэкенды с пассивными health checks
server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
# Резервный сервер - получает трафик только при отказе основных
server 192.168.1.12:8080 backup;
# Пул keepalive-соединений
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
# Включаем HTTP/1.1 для keepalive
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_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
Сценарий работы при сбое:
- Бэкенд 192.168.1.10 перестаёт отвечать.
- Nginx пытается отправить запрос, получает ошибку соединения. Счётчик неудач: 1.
- Следующие два запроса тоже завершаются ошибкой. Счётчик достигает
max_fails=3. - Nginx исключает сервер из пула на 30 секунд и направляет трафик на 192.168.1.11.
- Если 192.168.1.11 тоже отказывает, активируется резервный сервер 192.168.1.12.
- Через 30 секунд Nginx пробует отправить запрос на 192.168.1.10. При успешном ответе сервер возвращается в пул.
Keepalive-соединения снижают задержку для каждого запроса, проходящего через эту схему. При восстановлении бэкенда пул соединений перестраивается автоматически.
Если вы строите API Gateway с расширенной маршрутизацией, обратитесь к руководству по настройке отказоустойчивого API Gateway. Там разобраны health checking, failover и кеширование для Nginx Plus, Kong и Envoy Proxy.
Типичные ошибки и как их избежать
Ошибка 1: забытый заголовок Connection при настройке keepalive. Симптом: keepalive не работает, соединения закрываются после каждого запроса. Решение: добавить proxy_set_header Connection "" в блок location. Без этой строки заголовок Connection от клиента пробрасывается бэкенду и переопределяет поведение.
Ошибка 2: слишком агрессивные health checks. Симптом: бэкенды исключаются из пула при кратковременных скачках задержки. Решение: увеличить fail_timeout и уменьшить max_fails. Например, max_fails=5 fail_timeout=60s даёт бэкенду больше времени на восстановление перед исключением.
Ошибка 3: нехватка памяти из-за избыточного keepalive. Симптом: Nginx потребляет гигабайты памяти, хотя трафик не вырос. Решение: уменьшить значение keepalive в upstream. Проверить текущее количество соединений командой ss -tnp | grep nginx | wc -l. Если счётчик переваливает за 5000 при скромной нагрузке, пул keepalive нужно сократить.
Ошибка 4: неверные таймауты proxy. Симптом: клиенты получают ошибки 504 Gateway Timeout при нормально работающих бэкендах. Решение: проверить proxy_read_timeout и proxy_connect_timeout. Значение proxy_read_timeout 60s подходит для большинства приложений, но для долгих операций (генерация отчётов, загрузка файлов) требуется увеличить до 300 секунд и более.
Ошибка 5: отсутствие резервного сервера. Симптом: при отказе всех основных бэкендов клиенты получают ошибки, хотя есть свободные мощности. Решение: добавить сервер с флагом backup в блок upstream. Резервный сервер может быть менее мощным - он нужен для поддержания доступности на время восстановления основных узлов.
Ошибка 6: использование ip_hash с динамическими IP клиентов. Симптом: неравномерное распределение нагрузки, перекос на один бэкенд. Решение: если клиенты выходят через NAT или используют мобильные сети, их IP-адреса меняются. В этом сценарии лучше использовать least_conn или хранить сессии в Redis, а не привязываться к IP.
Для углублённого изучения отказоустойчивых конфигураций с health checks и плавным обновлением бэкендов без потери сессий используйте руководство по продвинутой настройке высокой доступности Nginx.