Перейти к содержанию

Обратный прокси Nginx и диагностика 502/504

502 Bad Gateway или 504 Gateway Timeout от nginx означает, что сам nginx работает нормально — он успешно получил запрос и сообщает вам, что бэкенд, к которому он проксирует, не ответил корректно (502) или не ответил вовремя (504). Это один из самых частых реальных продакшн- инцидентов, и почти всегда диагностируется меньше чем за пять минут, если проверять в правильном порядке.

Как это устроено

Минимальный конфиг обратного прокси

# /etc/nginx/sites-available/app.conf
upstream app_backend {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://app_backend;
        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_read_timeout 60s;
    }
}
sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
sudo nginx -t                          # проверка синтаксиса ПЕРЕД reload — тот же принцип, что у sshd -t
sudo systemctl reload nginx            # reload, не restart — применение конфига без простоя

proxy_set_header X-Forwarded-For важен для всего нижестоящего, что нуждается в реальном IP клиента (логи доступа, rate limiting, geo-IP) — без него бэкенд всегда видит в качестве источника запроса только сам IP nginx.

Диагностика 502 Bad Gateway

502 означает, что nginx вообще не смог получить валидный ответ от бэкенда — обычно потому, что процесс бэкенда не запущен, не слушает ожидаемый порт, или резко оборвал соединение.

# 1. Вообще что-то слушает там, где ожидает nginx?
ss -tlnp | grep 8000

# 2. Посмотреть собственный лог ошибок nginx на предмет конкретной причины
sudo tail -50 /var/log/nginx/error.log

connect() failed (111: Connection refused) в логе ошибок означает, что на этом порту вообще никто не слушает — процесс бэкенда упал, не запущен, или слушает другой порт/интерфейс, чем настроено. (104: Connection reset by peer) означает, что что-то слушало, но резко оборвало соединение посреди запроса — часто само приложение падает именно на этом запросе (проверяйте дальше логи приложения, не nginx).

Диагностика 504 Gateway Timeout

504 означает, что nginx действительно подключился к бэкенду, но не получил ответ в пределах proxy_read_timeout (по умолчанию 60с). Это указывает, что бэкенд жив, но слишком медленный, а не отсутствует.

# Бэкенд медленный или вообще завис?
time curl -sS http://127.0.0.1:8000/health    # вызвать его напрямую, минуя nginx

Если прямой curl тоже зависает дольше таймаута, проблема точно в самом приложении бэкенда (проверяйте его собственные логи, исчерпание пула потоков/воркеров, медленный нижестоящий запрос к БД, которого оно ждёт). Если прямой вызов возвращается быстро, а nginx всё равно отваливается по таймауту, подозревайте само значение proxy_read_timeout — слишком низкое для легитимно медленного эндпоинта, либо проблему именно на хопе nginx → бэкенд.

Основы балансировки нагрузки

upstream app_backend {
    least_conn;
    server 10.0.0.11:8000;
    server 10.0.0.12:8000;
    server 10.0.0.13:8000 backup;
}

least_conn отправляет новые соединения на тот бэкенд, у которого сейчас меньше всего активных соединений (в отличие от round-robin по умолчанию, который вообще игнорирует нагрузку бэкенда). backup помечает сервер как используемый только тогда, когда все не-backup серверы недоступны — полезно для резервного инстанса.

Различия Ubuntu/Debian и RHEL-подобных систем

Ubuntu/Debian RHEL-подобные
Расположение конфигов /etc/nginx/sites-available/ + симлинки в sites-enabled/ /etc/nginx/conf.d/*.conf (без разделения enabled/available по соглашению)
SELinux Н/Д Обратный прокси на нестандартный порт может потребовать setsebool httpd_can_network_connect on, иначе SELinux заблокирует прокси-соединение независимо от правил файрвола

Практическое задание

  1. Запустите тривиальный бэкенд: python3 -m http.server 8000.
  2. Настройте nginx на проксирование к нему (как в примере выше) и убедитесь, что работает через curl http://localhost/.
  3. Остановите Python-сервер (Ctrl+C) и убедитесь, что nginx теперь возвращает 502; проверьте error.log на точную причину.
  4. Перезапустите бэкенд, но сделайте его искусственно медленным (например, python3 -c "import time; time.sleep(120)" за кастомным хендлером, или просто временно выставьте proxy_read_timeout 2s;) и убедитесь в 504.

Как проверить, что всё сделано правильно

  • Шаг 3: HTTP-ответ должен быть ровно 502, а новая строка в error.log — упоминать Connection refused со ссылкой на адрес и порт бэкенда.
  • Шаг 4: ответ должен быть 504 после ожидания настроенного proxy_read_timeout, не раньше.

Типичные ошибки

  • Перезапуск nginx вместо reload для изменения конфига. reload перечитывает конфиг без потери ни одного соединения; restart ненадолго полностью закрывает слушающий сокет.
  • Увеличение proxy_read_timeout как первая реакция на 504, без проверки, не завис ли бэкенд на самом деле (бесконечный цикл, заблокированный воркер), а не просто легитимно медленный — более долгий таймаут на реально зависшем бэкенде просто заставляет клиентов дольше ждать того же сбоя.
  • Не проверять собственные логи приложения, только логи nginx — лог ошибок nginx говорит что бэкенд не ответил, редко почему сам бэкенд упал.

⚠ Опасные команды

  • systemctl restart nginx на продакшн-хосте с активными долгоживущими соединениями (WebSockets, long-polling) немедленно обрывает их все — reload почти всегда правильный выбор для изменений конфига; restart оставьте на случай, когда сам nginx реально завис.
  • Тестирование нового proxy_pass upstream в продакшне без nginx -t сначала может положить весь сайт целиком из-за опечатки, а не только изменяемый маршрут (синтаксическая ошибка в одном блоке server может помешать nginx перезагрузиться вообще, оставив работать предыдущий конфиг до исправления — это на самом деле защитная особенность reload, но только если никогда не обходить -t принудительным убийством и перезапуском).

Связанные статьи

Контрольные вопросы

  1. В чём практическая разница между тем, что сообщают 502 и 504 о месте сбоя?
  2. Почему nginx -t всегда нужно запускать перед reload, даже хотя сам reload описывается как «безопасный»?
  3. Почему увеличение proxy_read_timeout не исправляет бэкенд, который реально завис в дедлоке, хотя «исправляет» симптом в виде 504?
  4. В чём операционная разница между systemctl reload nginx и systemctl restart nginx для уже подключённых клиентов?