1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

5xx: 502 против 504

Коротко

502 и 504 путают чаще всех остальных кодов вместе: 502 - разговор не состоялся или оборвался, 504 - nginx ждал и не дождался. А ещё у обоих есть общая граница, о которой почти нигде не пишут.

  • Подменить код на 502 или 504 nginx может только до первого байта заголовков ответа. После - клиент получает 200 с обрезанным телом, и в access_log никакой ошибки нет.
  • no live upstreams невозможен, если в группе один сервер: единственный сервер nginx нерабочим не считает.
  • proxy_read_timeout меряет паузу между чтениями, а не весь ответ.
  • Отказ по лимиту пишется на уровне error, ожидание в очереди - на warn. Это две разные строки и два разных исхода для пользователя.

Если граница «до первого байта» и разница трёх таймаутов знакомы - листай до «Теперь сам».

Сначала ответь сам

В error_log за час сотня строк:

↳  выводgrep prematurely /var/log/nginx/error.log
[error] *20 upstream prematurely closed connection while reading upstream

В access_log за тот же час ни одного 5xx. Все коды 200. Кто врёт?

Никто. Бэкенд успел отдать заголовки, nginx успел переслать их клиенту вместе с кодом 200 - и только потом соединение оборвалось. Поменять уже отправленный код нечем.

Замер на стенде: бэкенд обещает Content-Length: 100, отдаёт 4 байта и рвёт соединение. В логе:

↳  выводgrep halfdrop /var/log/nginx/main.log
"GET /api/halfdrop HTTP/1.1" 200 4 rt=0.202 upstream=172.24.0.2:8000 us=200

Код 200, четыре байта из обещанных ста. Пользователь видит битую страницу, мониторинг по кодам ответа - идеальную картину.

На что это похоже

Диспетчер соединяет тебя со специалистом. Пока соединения нет, он может сказать «специалист не отвечает» или «линия занята» - у него есть свои слова для отказа.

Но как только специалист снял трубку и произнёс «здравствуйте», диспетчер отходит в сторону. Если связь оборвётся на третьем слове, ты услышишь тишину, а не «специалист не отвечает»: разговор уже начался, и подменить его нечем.

Первое слово специалиста - это и есть заголовки ответа. До них у nginx есть свои коды, после - только пересылка.

Механизм: точка невозврата

первый байт заголовков от бэкенда nginx решает сам не соединился - 502 оборвалось до заголовков - 502 не дождался - 504 есть запасной сервер - повтор nginx только пересылает код уже ушёл клиенту обрыв - тело обрезано, код 200 таймаут - то же самое повторить попытку нельзя
Слева ошибка видна в access_log кодом. Справа - только в error_log и по недосчитанным байтам; это и есть причина, по которой мониторинг «по кодам ответа» пропускает целый класс поломок.

Правило

502 и 504 - это диагноз nginx о попытке поговорить с бэкендом. Нет ответа вовсе - 502; ответ не пришёл вовремя - 504; ответ начался и сломался - код остаётся тот, что уже ушёл.

Разбор: четыре строки за кодом 502

Проверено на стенде, все четыре воспроизводятся:

↳  выводgrep -v keepalive /var/log/nginx/error.log
[error] connect() failed (111: Connection refused) while connecting to upstream
[error] upstream prematurely closed connection while reading response header from upstream
[error] upstream sent too big header while reading response header from upstream
[error] no live upstreams while connecting to upstream
Строка Что случилось Куда смотреть
Connection refused на порту никто не слушает бэкенд не запущен, упал или слушает другой адрес
prematurely closed ... response header бэкенд принял запрос и умер, не начав отвечать падение воркера, OOM (система убила процесс за нехватку памяти), таймаут внутри приложения
too big header заголовки ответа не влезли в буфер proxy_buffer_size, а заодно разобраться с раздутыми куками
no live upstreams все серверы группы помечены нерабочими сначала чинить бэкенды, потом смотреть max_fails

Сравни вторую строку со строкой из начала урока: они отличаются хвостом. while reading response header значит «до заголовков» и даёт 502; та же строка с хвостом while reading upstream значит «после» и оставляет клиенту код, который уже ушёл.

Отдельная ловушка: бэкенд слушает 127.0.0.1, а nginx стучится на localhost, который резолвится (превращается в адрес) в ::1 - IPv6-адрес этой же машины. Порт открыт, но не тот стек, и в логе всё то же Connection refused. Пиши адрес явно.

Разбор: где наивное правило подводит

no live upstreams не бывает при одном сервере. Строка выглядит так, будто её причина - смерть бэкенда, и она действительно про это. Но замер показывает другое: группа из одного мёртвого сервера три запроса подряд даёт Connection refused и ни разу не даёт no live upstreams. nginx не выключает единственный сервер группы, каким бы мёртвым тот ни был - выключить его значило бы гарантированно отвечать 502 даже после того, как бэкенд поднялся.

С двумя серверами появляется полная цепочка:

↳  выводтри запроса в группу из двух мёртвых серверов
2026/09/10 19:59:03 [error] 8#8: *17 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://127.0.0.1:9/dead/x", host: "localhost"
2026/09/10 19:59:03 [warn] 8#8: *17 upstream server temporarily disabled while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://127.0.0.1:9/dead/x", host: "localhost"
2026/09/10 19:59:03 [error] 8#8: *17 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://127.0.0.1:10/dead/x", host: "localhost"
2026/09/10 19:59:03 [warn] 8#8: *17 upstream server temporarily disabled while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://127.0.0.1:10/dead/x", host: "localhost"
2026/09/10 19:59:03 [error] 8#8: *20 no live upstreams while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://dead2/dead/x", host: "localhost"
2026/09/10 19:59:03 [error] 8#8: *21 no live upstreams while connecting to upstream, client: 127.0.0.1, server: shop.local, request: "GET /dead/x HTTP/1.1", upstream: "http://dead2/dead/x", host: "localhost"

Номер после звёздочки - номер соединения. Первый запрос (*17) попробовал оба сервера - две пары строк - и получил 502; второй и третий застали оба выключенными и сразу получили no live upstreams. Читается цепочка снизу вверх: no live upstreams - это следствие, а причина - в строках выше, и там же написано, чем именно серверы не понравились.

Таймаут - это пауза, а не общее время. proxy_read_timeout отсчитывается заново после каждой порции данных. На стенде бэкенд с proxy_read_timeout 5s отдавал ответ по кусочку раз в три секунды и уложился в 18 секунд без единой ошибки. Обратное тоже верно: бэкенд, который молчит шесть секунд и потом отвечает мгновенно, в тот же таймаут упрётся.

Этап, на котором истекло время, написан в строке:

Фраза в логе Директива Умолчание
while connecting to upstream proxy_connect_timeout 60с (больше 75с поставить нельзя); таймаут на любом из трёх этапов даёт 504
while sending request to upstream proxy_send_timeout 60с
while reading response header from upstream proxy_read_timeout 60с

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

limit_req ограничивает частоту запросов с одного адреса; zone "perip" - имя области памяти, где nginx ведёт счёт, excess - на сколько запрос превысил разрешённую частоту (подробно - в разделе про лимитыРазбирается в разделе 12, глава «limit_req и limit_conn»: Как nginx считает частоту).

↳  выводlimit_req rate=1r/s: первая строка без burst, вторая с burst=3
[error] limiting requests, excess: 0.992 by zone "perip"
[warn]  delaying request, excess: 0.991, by zone "perip"

По первой строке клиент получил 503, по второй - 200, подождав. limiting на уровне error - запрос отвергнут. delaying на уровне warn - запрос поставлен в очередь и всё-таки выполнен. Если в логе только delaying, отказов не было вообще, и поднимать лимит незачем: люди просто ждут чуть дольше.

Что ломается без этого

Мониторинг «доля 5xx» показывает ноль, а пользователи видят обрезанные страницы. Это прямое следствие точки невозврата: пока обрывы случаются после заголовков, кодов ошибок не будет. Ловить такое приходится иначе - по error_log и по расхождению $body_bytes_sent с обещанным размером - его бэкенд пишет в Content-Length, а в лог он попадает переменной $upstream_http_content_length.

Второе: команда сутки чинит max_fails, увидев no live upstreams, вместо того чтобы прочитать три строки выше и узнать, что бэкенды отвергали соединения.

Зачем это в работе

Три строки, которые надо узнавать с первого взгляда, потому что означают «плохо всем сразу»:

↳  выводgrep -E "worker_connections|accept4" /var/log/nginx/error.log
[warn] 9#9: 64 worker_connections are not enough, reusing connections
[alert] 10#10: *10 4 worker_connections are not enough while connecting to upstream
[crit] 10#10: accept4() failed (24: No file descriptors available)

Три строки об одном пределе, и означают они разное - это видно по уровню и по хвосту.

warn с хвостом reusing connections - nginx выкрутился: закрыл простаивающие keepalive-соединения и обслужил запрос. Замер при worker_connections 64 под нагрузкой: accepts 120, handled 120 - не потеряно ни одного соединения. Это счётчики страницы stub_status: сколько соединений принято и сколько обработано.

alert без «reusing» - слота не нашлось вовсе. Клиент получает 500, а при сильной нехватке соединения отбрасываются до чтения запроса: замер при worker_connections 4 дал accepts 38, handled 31, и этих семи в access_log нет ни строкой.

crit с accept4() - кончились уже дескрипторы процесса, а не слоты nginx. Текст зависит от библиотеки: на alpine No file descriptors available, на debian Too many open files, номер ошибки в обоих случаях 24. Лечится worker_rlimit_nofile плюс лимитом в юните systemd - одного конфига nginx мало (как поднять лимит в юните, разобрано в уроке про число воркеров). О расхождении nginx предупреждает ещё при старте; ulimit -n 40 - команда оболочки, которая ставит процессу предел в 40 открытых файлов, свой смотри через ulimit -n без числа:

↳  выводnginx -t при ulimit -n 40
nginx: [warn] 64 worker_connections exceed open file resource limit: 40

Порядок разбора любого 5xx:

  1. Что в $upstream_status - бэкенд ответил ошибкой сам или его не спросили?
  2. Что в error_log в ту же секунду - refused, timed out, prematurely closed?
  3. Есть ли в строке слово header - оборвалось до заголовков или после?
  4. Это один запрос, один адрес клиента или все сразу?

Поднять таймаут - не починка

Первый рефлекс при 504 - написать proxy_read_timeout 300s;. Иногда это верно (тяжёлый отчёт, экспорт), но чаще так прячут проблему: пользователь всё равно не ждёт пять минут, а воркеры и соединения к базе держатся занятыми всё это время, и под нагрузкой сервис ложится целиком. Сначала посмотри $upstream_header_time в логах: если он стабильно растёт, лечить надо бэкенд.

Теперь сам

Бэкенд перезапускается на выкладке, это занимает восемь секунд. proxy_read_timeout - стандартные 60. Какой код увидят пользователи в эти восемь секунд и что будет в $upstream_response_time?

502, а не 504, и urt=0.000. Ждать нечего: порт закрыт, соединение отвергается сразу, в логе connect() failed (111: Connection refused). Ждать можно только того, кто ответил хоть что-то. И если в группе есть второй живой сервер, части пользователей повезёт: nginx сходит на него, а в строке останется us=502, 200.

Главное

502 - соединения не было или оно оборвалось до заголовков (refused, prematurely closed ... header, no live upstreams, too big header); 504 - не дождался, и этап написан в строке. После первого байта заголовков подменить код нечем: обрыв даёт 200 с обрезанным телом и виден только в error_log. no live upstreams невозможен при единственном сервере в группе. proxy_read_timeout меряет паузу между чтениями, а не весь ответ. limiting requests на уровне error - отказ, delaying request на warn - очередь.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.