Ошибка nginx 504 Gateway Time-out: ищем причину, а не поднимаем таймаут

Опубликовано 17 мин. чтения

При ошибке 504 бэкенд был доступен, он лишь ответил слишком медленно. Как по журналу с замерами времени определить, куда уходит время, какой из множества сроков действительно срабатывает и почему поднятый таймаут чаще всего просто отодвигает отказ.

504 Gateway Time-out — самая терпеливая из всех страниц ошибок. nginx принял запрос, передал его бэкенду и после этого ждал, пока не истёк внутренне заданный срок. Бэкенд всё это время оставался доступен, он просто не ответил вовремя. Именно в этом и состоит отличие от соседнего кода: при 502 Bad Gateway бэкенд отвечает неправильно или не отвечает вовсе, при 504 он отвечает слишком медленно. Это руководство показывает, как измерить, куда на самом деле уходит время, какой из множества сроков вообще срабатывает и почему поднятие этого предела почти всегда оказывается худшим из доступных решений.

Все сведения относятся к Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS и Ubuntu 22.04 LTS. Команды рассчитаны на работу от имени root, обычному пользователю нужно ставить впереди sudo. В примерах стоит PHP 8.4, заменяйте номер версии на тот, что установлен в вашей системе:

СистемаPHPСлужбаКонфигурация
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/
ls /etc/php/

Директивы nginx во всех четырёх системах называются одинаково. Различия лежат на стороне PHP и базы данных, они отмечены в соответствующих местах.

Кто именно сдался, тот и задаёт направление поиска

Прежде чем открывать хоть один файл, ответьте на один вопрос: какой слой оборвал работу? Код статуса выдаёт это сразу.

КодЧто произошлоГде искать
500 Internal Server ErrorБэкенд ответил, но ответом была ошибкаЖурнал приложения
502 Bad GatewayСоединение не установилось или оборвалосьСлужба, сокет, права, падения
504 Gateway Time-outСоединение стояло, ответ не пришёл в отведённый срокВремя выполнения на бэкенде
408 Request TimeoutПосетитель не успел дослать до конца собственный запросЗагрузка файлов, медленные подключения
499 (только в журнале)Посетитель прервал запрос раньше, чем nginx закончилСлишком медленно, но в пределах срока

Строка с 499 недооценена сильнее всего. Это не ошибка, а система раннего предупреждения: посетитель закрыл вкладку, потому что страница грузилась для него слишком долго. Если 504 исчезает после поднятия срока, а вместо него появляются 499, значит, не решено ничего.

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Девятое поле относится к стандартному формату combined. Много 504 при малом числе 499 указывает на отдельные тяжёлые страницы, обратная картина — на приложение, которое медленное целиком.

Прежде чем что-то менять: путь назад

Диагностика в следующих двух разделах только читает и ничего не меняет. Рискованно становится там, где вы трогаете конфигурации, и остановить работу это может тремя путями: ошибочная конфигурация nginx не даёт стартовать веб-серверу, ошибочный файл пула не даёт стартовать PHP-FPM, а щедро поднятые пределы способны исчерпать оперативную память. Последний случай самый неприятный, потому что процессы завершает тогда ядро, и достаётся при этом не обязательно виновнику. Если под раздачу попадёт служба SSH, сервером уже не получится управлять по сети.

Поэтому сначала сделайте копии, внутри /root и никогда в веб-каталоге. Отметка времени в имени важна, потому что вмешиваться редко приходится всего один раз, а cp -a перезаписывает уже существующую копию без единого вопроса:

mkdir -p /root/backups
cp -a /etc/nginx/nginx.conf /root/backups/nginx.conf.$(date +%F-%H%M)
cp -a /etc/nginx/sites-available/example.com /root/backups/example.com.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/php.ini /root/backups/php.ini.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)

Путь назад состоит из трёх строк, и порядок здесь выбран намеренно:

cp -a /root/backups/example.com.2026-09-03-1030 /etc/nginx/sites-available/example.com
nginx -t
systemctl reload nginx

Пользуйтесь reload вместо restart, пока это возможно. reload принимает новую конфигурацию только тогда, когда в ней нет ошибок. restart сначала завершает работающий процесс и при ошибке оставляет вас вообще без веб-сервера.

Если сервер перестал отвечать совсем, откройте на KVM root-серверах и выделенных серверах KernelHost консоль VNC в личном кабинете. Она подключена к слою виртуализации, соответственно к самому порту, а не к сетевому стеку гостевой системы, поэтому работает и тогда, когда ни одна служба уже недоступна. Зайдите туда заранее один раз и убедитесь, что пароль root вам известен. Проверочная команда после каждого вмешательства:

systemctl is-active nginx php8.4-fpm
free -m

Строка в журнале ошибок, которая решает дело

504 всегда оставляет след:

2026/09/03 10:12:33 [error] 812#812: *5 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.7, server: example.com,
request: "GET /report.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"

Решает не номер ошибки 110, который при каждом 504 одинаков, а фаза после него:

  • while connecting to upstream: соединение так и не установилось. При удалённом бэкенде это почти всегда пакетный фильтр, который отбрасывает пакеты вместо того, чтобы их отклонять, ведь отказ вернулся бы немедленно и дал бы 502.
  • while sending request to upstream: nginx не смог передать тело запроса, типично при загрузке больших файлов.
  • while reading response header from upstream: обычный случай. Бэкенд получил всё и считает, не отправив даже первой строки заголовка.
  • while reading upstream: заголовки пришли, а затем застопорилось тело. Такое вы увидите при потоковых ответах и выгрузках.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Многие виртуальные хосты пишут в собственный журнал ошибок. Где лежит этот файл и почему при поиске в sites-enabled нужна заглавная -R, написано в статье про 502. Если поиск остаётся пустым, хотя браузер показывает 504, ошибка исходит не от этого nginx.

Куда уходит время: измерять вместо догадок

nginx умеет записывать по каждому запросу, сколько времени потратил бэкенд. Это важнейший шаг всей диагностики, потому что он отвечает на вопрос «приложение или канал» без всяких догадок. В блоке http файла /etc/nginx/nginx.conf:

log_format kh_timing '$time_iso8601 $status rt=$request_time '
                     'uct=$upstream_connect_time uht=$upstream_header_time '
                     'urt=$upstream_response_time "$request"';

В нужном блоке server добавьте вторую строку записи. Существующий журнал доступа остаётся нетронутым, nginx пишет оба:

access_log /var/log/nginx/timing.log kh_timing;
nginx -t
systemctl reload nginx
tail -n 5 /var/log/nginx/timing.log

После нескольких минут работы поднимите наверх самые медленные запросы:

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
НаблюдениеТолкованиеСледующий шаг
Высокий uct при локальном бэкендеБуксует установление соединенияПереполненная очередь на сокете, медленное разрешение имён
uht и urt почти равны, оба высокиеБэкенд считает, прежде чем отправить первую строку заголовкаПриложение, база данных, сторонний API
uht маленький, urt высокийЗаголовок пришёл быстро, тело сочится по каплеПотоковая отдача, выгрузки, циклы по большому числу записей
urt маленький, rt высокийБэкенд отработал быстро, время потерялось уже после негоКанал посетителя, очень большой ответ
Дефис вместо числаБэкенд вообще не участвовалСтатический файл или обрыв до передачи запроса

Две тонкости экономят массу времени. Несколько значений через запятую в одном поле означают, что запрос ушёл более чем к одной цели, то есть была повторная попытка. И самая точная подсказка из всех: если измеренная длительность совпадает с настроенным значением до секунды, например 60,001 секунды при сроке в 60, значит, сработал именно срок, а бэкенд сам не сдавался. Некруглые значения вроде 43,7 секунды показывают, что тормозило что-то другое.

Какой из сроков вообще срабатывает

У nginx для таймаутов есть добрая дюжина директив, и чаще всего впустую потраченный час возникает оттого, что человек трогает не ту. Какая из них сработает, зависит от того, какой модуль обрабатывает блок location.

ДирективаПо умолчаниюДействует в блоках сЧто происходит по истечении
proxy_connect_timeout60sproxy_pass504, «while connecting to upstream»
proxy_send_timeout60sproxy_pass504, «while sending request to upstream»
proxy_read_timeout60sproxy_pass504, решающий срок при проксируемых бэкендах
fastcgi_connect_timeout60sfastcgi_pass504, как выше, для PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, как выше
fastcgi_read_timeout60sfastcgi_pass504, решающий срок при PHP
send_timeout60sвезденикакого 504, закрывается соединение с посетителем
client_body_timeout60sвезде408, а не 504

Срок действует между двумя операциями чтения, а не на весь ответ целиком. Скачивание, которое длится десять минут и всё это время непрерывно отдаёт данные, проходит спокойно. Бэкенд, который молчит 61 секунду, вылетает. Поэтому при спотыкающихся выгрузках чаще помогает заставить приложение регулярно что-нибудь отдавать, чем поднимать срок.

send_timeout против 504 не делает ничего. Этот срок касается передачи данных посетителю. Когда он истекает, никакой страницы ошибки не появляется, вместо этого обрывается скачивание. Значимым он становится лишь тогда, когда вы отключаете буферизацию через proxy_buffering off;, ведь тогда медленный посетитель тормозит всю цепочку вплоть до бэкенда.

nginx принимает и такие директивы, которые в данном блоке не действуют. Строка proxy_read_timeout 300s; в блоке PHP с fastcgi_pass синтаксически безупречна, nginx -t сообщает syntax is ok, а страница по-прежнему обрывается через 60 секунд. В обратную сторону точно так же. Это самая частая причина того, что поднятый срок остаётся без эффекта. Что действует на самом деле, показывает собранная воедино конфигурация:

nginx -T | grep -E "read_timeout|send_timeout|fastcgi_pass|proxy_pass"

Если отдельному пути действительно нужно работать дольше, задайте срок ровно там и больше нигде. Знак равенства превращает это в точное совпадение, а оно выигрывает у общего блока location ~ \.php$, который обрабатывает остальные файлы PHP:

location = /admin/export.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_read_timeout 300s;
}
nginx -t
systemctl reload nginx
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/admin/export.php

PHP: почему max_execution_time здесь срабатывает редко

Напрашивается предположение, что PHP сам завершит зависший скрипт. Именно в этом случае оно чаще всего неверно. Сначала ловушка при измерении: php -i опрашивает вариант для командной строки. Тот использует собственную конфигурацию в /etc/php/8.4/cli/ и в любом случае работает без ограничения времени выполнения, то есть об FPM не говорит ничего. Решают два других места:

grep -n "^max_execution_time" /etc/php/8.4/fpm/php.ini
grep -rn "max_execution_time" /etc/php/8.4/fpm/pool.d/

В файле пула значение может быть перекрыто через php_value[max_execution_time] или php_admin_value[max_execution_time]. Значение, заданное через php_admin_value, из приложения уже не изменить вызовом ini_set(). Если ваш фреймворк сам поднимает время выполнения и это вдруг перестало действовать, причина именно здесь.

А теперь главное: под Linux время ожидания в системных вызовах не засчитывается. Часы идут только тогда, когда скрипт считает сам. Стоит ему начать ждать запрос к базе данных, сторонний API или файловую систему, и они останавливаются. Так скрипт может десять минут висеть на зависшем запросе, а ограничение времени выполнения так ни разу и не сработает. Завершает его тогда срок nginx, и результатом становится 504.

Отсюда следует неудобное правило: max_execution_time защищает от бесконечных циклов в собственном коде, но не от ожидания. Единственный жёсткий предел на стороне PHP — это request_terminate_timeout в файле пула, который убирает рабочий процесс независимо от того, на чём тот завис. Правда, он даёт 502, а не 504. Сроки расставляйте по возрастанию изнутри наружу, чтобы первым срабатывал тот слой, который ещё способен выдать понятное сообщение. Для считающих скриптов это работает, для ждущих, по только что названной причине, нет. Там остаётся одно: ограничивать само время ожидания.

Почему увеличение срока обычно оказывается неверным решением

Посчитайте сами. В пуле с pm.max_children = 10 десять рабочих процессов. Странице нужно 90 секунд. Десять одновременных обращений тем самым занимают на полторы минуты каждый из них. В это время никто больше не получит ни одной страницы PHP, в том числе и стартовой. Из медленной внутренней страницы получился отказ всего сайта. Усиливают это три механизма:

  • Посетитель перезагружает страницу. Это создаёт дополнительный запрос, но не освобождает старый. PHP замечает ушедшего посетителя только тогда, когда скрипт в следующий раз что-нибудь выведет, а считающий скрипт долго не выводит ничего.
  • nginx повторяет запрос сам. При блоке upstream с несколькими целями proxy_next_upstream по умолчанию стоит в error timeout. Запрос, у которого истёк срок, уходит к следующему серверу, и дорогая выборка отрабатывает второй раз. Пишущие запросы из этого исключены, читающие нет. Отключается через proxy_next_upstream error;.
  • Мониторинг повторяет тоже. Интервал проверки в 60 секунд для страницы, которой нужно 90 секунд, создаёт постоянную нагрузку, и разобрана она не будет никогда.

К этому добавляется то, что никто не ждёт веб-страницу пять минут. Срок в 300 секунд превращает минутную проблему в пятиминутную, посетитель давно ушёл, а рабочий процесс всё ещё считает.

Насколько всё занято на самом деле, показывает страница состояния PHP-FPM. Задайте в файле пула pm.status_path = /fpm-status и создайте в блоке server доступ, открытый только локально:

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
systemctl reload php8.4-fpm
nginx -t
systemctl reload nginx
curl -s -H "Host: example.com" http://127.0.0.1/fpm-status

Обратите внимание на active processes, listen queue и max active processes. Если listen queue постоянно держится выше нуля, числа рабочих процессов не хватает при нынешнем времени выполнения страниц. Это тот момент, когда снижать нужно время выполнения, а не поднимать срок.

Три обычных пожирателя времени

База данных

В большинстве случаев время уходит именно сюда. Сначала посмотрите, что выполняется прямо сейчас. Во всех четырёх системах доступ root по умолчанию идёт через сокет Unix, поэтому команда обходится без пароля:

mysql -e "SHOW FULL PROCESSLIST;"

Интересны столбцы Time и State. Значения вроде Sending data или Waiting for table metadata lock при двузначном числе секунд — это как раз ваш случай. Систематически искать помогает журнал медленных запросов, включаемый на ходу:

mysql -e "SET GLOBAL slow_query_log = 1; SET GLOBAL long_query_time = 1;"
mysql -e "SHOW VARIABLES LIKE 'slow_query_log_file';"

Имя файла возьмите из второго вывода, потому что оно различается: Debian обычно ставит MariaDB и пишет в /var/log/mysql/mariadb-slow.log, под Ubuntu файл называется иначе в зависимости от установленного сервера. Через несколько минут разберите результат и выключите журнал обратно, запись создаёт нагрузку на диск:

mysqldumpslow -s t /var/log/mysql/mariadb-slow.log | head -30
mysql -e "SET GLOBAL slow_query_log = 0;"

Ключ -s t сортирует по суммарному времени. Самый дорогой запрос проверьте через EXPLAIN, чаще всего не хватает индекса ровно на том столбце, по которому идёт фильтрация или сортировка. SET GLOBAL действует сразу, но не переживает перезапуска базы данных. Верхний предел на каждый запрос есть и на стороне базы данных: у MariaDB это max_statement_time в секундах, у MySQL max_execution_time в миллисекундах, причём последнее только для читающих запросов. Так ваше приложение получит внятную ошибку вместо занятого рабочего процесса.

Сторонние API

Если ваша страница при каждом обращении опрашивает платёжного провайдера или сервер лицензий, их сбой становится вашим 504. Измерьте этот вызов отдельно, прямо с сервера:

curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://api.example.com/status

Если подозрительно выглядит уже dns, дело в разрешении имён, а не в удалённой стороне, встречная проверка через time getent hosts api.example.com. В коде каждому внешнему вызову нужен собственный короткий срок: у cURL это CURLOPT_CONNECTTIMEOUT и CURLOPT_TIMEOUT. Кто вместо этого применяет к адресу file_get_contents(), попадает на default_socket_timeout из php.ini, а там по умолчанию стоят 60 секунд:

grep -n "default_socket_timeout" /etc/php/8.4/fpm/php.ini

Правило простое: сумма всех внешних сроков одного запроса должна оставаться ниже fastcgi_read_timeout. Иначе nginx обрежет соединение раньше, чем ваш код успеет выдать понятную страницу ошибки.

Файловая система и оперативная память

Заполненный накопитель делает запись медленной или вовсе невозможной, и бьёт это одновременно по файлам сессий, по кэшу и по журналам. Проверьте и то и другое, свободное место и inode:

df -h
df -i

Вторую команду забывают чаще всего. Раздел может быть занят на 40% и всё равно не принимать больше ни одного файла, если inode исчерпаны. Второй кандидат — нехватка памяти: когда система начинает выгружать страницы в swap, вязким становится каждый запрос, и виноват при этом не какой-то конкретный запрос к базе.

vmstat 1 5

Если столбцы si и so постоянно держатся выше нуля, ядро непрерывно выгружает и подгружает страницы, и проблема у вас с памятью, а не со временем. Как обращаться с этим правильно, описано в статье настройка swap и как избежать Out-of-Memory. Третий кандидат — сетевые диски: зависшая точка монтирования NFS блокирует любой процесс, который к ней обращается, и ваша диагностическая команда в их числе. Поэтому ставьте перед ней срок:

timeout 5 df -h

Долгим задачам не место внутри запроса

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

  1. Запрос создаёт задание, в таблице или в очереди, и отвечает сразу. Подходящий код статуса — 202, вместе с адресом, по которому можно узнать состояние.
  2. Рабочий процесс за пределами nginx забирает задания и выполняет их. Никакой срок его не подгоняет, потому что никто его не ждёт.
  3. Интерфейс опрашивает состояние. Этот опрос всегда быстрый, независимо от того, сколько длится сама задача.

Рабочий процесс запускайте как отдельную службу, чтобы после падения и после перезагрузки он поднимался сам. Как выглядит такой юнит, показывает статья создание службы systemd. Для задач через равные промежутки хватит cron-задания, там вам понадобится блокировка, чтобы два запуска не обгоняли друг друга. С ключом -n второй запуск сразу прерывается вместо того, чтобы ждать:

flock -n /run/lock/kh-worker.lock /usr/bin/php /var/www/html/worker.php

Для распространённых фреймворков очередь есть в готовом виде, её остаётся только запустить: у Laravel это php artisan queue:work, у Symfony php bin/console messenger:consume с именем вашего транспорта. И то и другое должно жить в systemd-юните, а не в окне терминала.

Один особый случай заслуживает отдельного упоминания: WordPress по умолчанию запускает запланированные задачи внутри запросов посетителей. То есть посетитель расплачивается своим временем ожидания за то, что в фоне идёт проверка обновлений. Строка define('DISABLE_WP_CRON', true); в wp-config.php, выше подключения wp-settings.php, это отключает. После этого вызывайте наступившие задачи регулярно сами:

wp cron event run --due-now --path=/var/www/html

То, что обязано остаться синхронным, получает собственный блок location с собственным сроком и вдобавок ограничение, чтобы этот один путь не занял все рабочие процессы: limit_conn_zone $binary_remote_addr zone=export:10m; в блоке http и limit_conn export 1; в нужном блоке. Дальнейшие попытки получат тогда 503 вместо занятого сервера.

Частые ошибки и их решения

Сообщение дословноЗначение и что делать
upstream timed out (110: Connection timed out) while reading response header from upstreamОбычный случай. Бэкенд считает слишком долго. Разберите журнал с замерами времени и определите пожирателя времени, прежде чем крутить срок.
upstream timed out (110: Connection timed out) while connecting to upstreamВ срок упёрлось установление соединения. При удалённом бэкенде это почти всегда пакетный фильтр, который отбрасывает вместо того, чтобы отклонять, при локальном PHP-FPM переполненная очередь на сокете.
upstream timed out (110: Connection timed out) while reading upstreamЗаголовки пришли, а затем тело застопорилось дольше отведённого срока. Типично для выгрузок, которые время от времени подолгу считают.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12Директива стоит вне http, server или location, чаще всего по недосмотру в самом начале файла.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31Опечатка. nginx проверяет имена, а не намерения. Номер строки указан прямо в сообщении.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Здесь ограничение времени выполнения PHP в виде исключения сработало, то есть скрипт считал, а не ждал. Даёт 500 или пустую страницу, но не 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionСтроку держит другая транзакция, срок по умолчанию составляет 50 секунд. Причина почти всегда в транзакции, которая остаётся открытой слишком долго.
cURL error 28: Operation timed out after 60000 millisecondsСторонний API не отвечает. Задайте в коде собственный, более короткий срок, чтобы ваше приложение сохранило контроль.
504 в браузере, но поиск по upstream timed out остаётся пустым504 исходит не от этого nginx, а от службы перед ним, например от балансировщика нагрузки или второго прокси. Некоторые из них сообщают об этом собственным кодом статуса.
Обрыв по-прежнему ровно через 60 секунд, хотя срок выставлен на 300Изменённая директива относится не к тому модулю, или выигрывает другой блок. nginx -T показывает, что действует на самом деле.

Как понять, что проблема действительно устранена

Команда без сообщения об ошибке не доказывает ничего, единичное удачное обращение тоже. Четыре доказательства, которые работают вместе:

  1. Код статуса и длительность прямо на сервере, чтобы результат не приукрасил какой-нибудь кэш. Ожидается 200 и длительность заметно ниже срока. Ответ 200 через 58 секунд при сроке в 60 — это не успех, а следующий отказ при чуть большей нагрузке. Считается при этом худшее из двадцати обращений:
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/report.php; done | sort -k2 -n | tail -3
  2. Журнал ошибок молчит. Обрежьте его перед тестом командой truncate -s 0 /var/log/nginx/error.log, выполните обращения, загляните туда снова. Пустой файл и есть настоящее доказательство.
  3. Очередь PHP-FPM стоит на нуле. Пока на странице состояния под listen queue что-то ждёт, причина всего лишь отодвинута.
  4. Перезагрузка ничего не меняет. Самый часто пропускаемый пункт. Значения из SET GLOBAL, запущенные вручную процессы и созданные вручную каталоги внутри /run её не переживают. Проверьте systemctl is-enabled nginx php8.4-fpm и перезагрузите сервер один раз контролируемо, пока вы ещё за ним смотрите.

Эту перезагрузку вместе с доступом к консоли на KVM root-серверах и выделенных серверах KernelHost вы выполняете в личном кабинете, даже если веб-служба уже ничего не отдаёт. Серверы стоят в дата-центре maincubes во Франкфурте-на-Майне (сертификация TÜV TIER3+), а фильтрация вынесена в сеть перед ними.

Краткий чек-лист для критической ситуации

  1. Посчитайте коды статуса в журнале доступа. 504, 499 и 502 рядом друг с другом говорят больше, чем каждый из них по отдельности.
  2. grep "upstream timed out" /var/log/nginx/error.log, выпишите фазу из сообщения.
  3. Включите журнал с замерами времени, сравните uct, uht и urt. Если длительность совпадает с настроенным сроком до секунды, сработал именно срок.
  4. Определите пожирателя времени: база данных, сторонний API, файловая система или память.
  5. Через nginx -T проверьте, какой срок действует в этом блоке, прежде чем менять хоть один.
  6. Только после этого решайте: устранять причину, выносить задачу в очередь или, как последний вариант, поднимать срок ровно для этого одного пути.
  7. После исправления: обрежьте журнал, измерьте двадцать обращений, один раз контролируемо перезагрузите сервер.

Частые вопросы

Чем 502 Bad Gateway отличается от 504 Gateway Time-out?
При 502 соединение с бэкендом не устанавливается или обрывается, то есть бэкенд отвечает неправильно либо не отвечает вовсе. При 504 соединение стояло, просто ответ не пришёл в отведённый срок: бэкенд всё это время оставался доступен и ответил слишком медленно. Код 500, напротив, означает, что бэкенд ответил, но ответом была ошибка. Отсюда следует и направление поиска: при 502 вы проверяете службу, сокет и права, при 504 время выполнения на бэкенде.
Я выставил fastcgi_read_timeout на 300 секунд, но страница всё равно обрывается через 60 секунд. В чём дело?
Почти всегда в том, что изменённая директива относится не к тому модулю или что выигрывает другой блок. nginx принимает и такие директивы, которые в данном блоке не действуют: строка proxy_read_timeout 300s; в блоке PHP с fastcgi_pass синтаксически безупречна, nginx -t сообщает «syntax is ok», а страница по-прежнему обрывается через 60 секунд. В обратную сторону точно так же. Что действует на самом деле, показывает собранная воедино конфигурация через nginx -T и поиск по read_timeout, send_timeout, fastcgi_pass и proxy_pass. Если отдельному пути нужно работать дольше, задайте срок в собственном блоке location со знаком равенства, ведь точное совпадение выигрывает у общего блока PHP.
Решает ли проблему увеличенный срок?
Чаще всего он её лишь отодвигает. В пуле с pm.max_children = 10 десять рабочих процессов. Если странице нужно 90 секунд, десять одновременных обращений занимают на полторы минуты каждый из них, и в это время никто больше не получит ни одной страницы PHP, в том числе и стартовой. Из медленной внутренней страницы получился отказ всего сайта. Вдобавок никто не ждёт веб-страницу пять минут. Если 504 исчезает после поднятия срока, а в журнале доступа вместо него появляются коды 499, значит, посетитель обрывал запрос сам, и не решено ничего.
Какая строка в журнале ошибок относится к 504?
504 всегда оставляет строку вида «upstream timed out (110: Connection timed out) while reading response header from upstream», найти её можно командой «grep -n "upstream timed out" /var/log/nginx/error.log». Решает не номер ошибки 110, который при каждом 504 одинаков, а фаза после него: «while connecting to upstream» означает, что соединение так и не установилось, «while sending request to upstream» говорит о том, что nginx не смог передать тело запроса, «while reading response header from upstream» описывает обычный случай, когда бэкенд считает, не отправив даже первой строки заголовка, а «while reading upstream» означает, что после заголовков застопорилось тело.
Почему max_execution_time не завершает мой зависший скрипт PHP?
Потому что под Linux время ожидания в системных вызовах не засчитывается. Часы идут только тогда, когда скрипт считает сам. Стоит ему начать ждать запрос к базе данных, сторонний API или файловую систему, и они останавливаются, а скрипт может десять минут висеть на зависшем запросе, и ограничение времени выполнения так ни разу и не сработает. Завершает его тогда срок nginx, и результатом становится 504. Учтите к тому же ловушку при измерении: php -i опрашивает вариант для командной строки, который использует собственную конфигурацию и в любом случае работает без ограничения времени выполнения. Решают php.ini от FPM и файл пула. Единственный жёсткий предел на стороне PHP — это request_terminate_timeout, но он даёт 502, а не 504.
Как отличить срабатывание срока от того, что бэкенд сдался сам?
По измеренной длительности. Записывайте через собственный log_format значения $request_time, $upstream_connect_time, $upstream_header_time и $upstream_response_time в отдельный файл. Если длительность совпадает с настроенным значением до секунды, например 60,001 секунды при сроке в 60, значит, сработал именно срок, а бэкенд сам не сдавался. Некруглые значения вроде 43,7 секунды показывают, что тормозило что-то другое. Несколько значений через запятую в одном поле означают повторную попытку ко второй цели, а дефис вместо числа означает, что бэкенд вообще не участвовал.
Браузер показывает 504, но поиск по «upstream timed out» ничего не находит. Откуда тогда ошибка?
Значит, 504 исходит не от этого nginx, а от службы перед ним, например от балансировщика нагрузки или второго прокси, и некоторые из них сообщают об этом собственным кодом статуса. Но сначала проверьте ещё одно: многие виртуальные хосты пишут в собственный журнал ошибок, так что вы вполне можете искать не в том файле.
Что делать с задачами, которые по своей природе длиннее любого разумного срока?
Вынесите их из запроса. Запрос создаёт задание и отвечает сразу, подходящим кодом статуса 202 и адресом, по которому можно узнать состояние. Рабочий процесс за пределами nginx выполняет задание, и никакой срок его не подгоняет, а интерфейс только опрашивает состояние. Запускайте этот рабочий процесс как отдельную службу, чтобы после падения и после перезагрузки он поднимался сам, и страхуйте повторяющиеся запуски через flock -n, чтобы два прохода не обгоняли друг друга. То, что обязано остаться синхронным, получает собственный блок location с собственным сроком и вдобавок ограничение через limit_conn, чтобы этот один путь не занял все рабочие процессы.

nginx PHP-FPM 504 Gateway Time-out таймаут Debian Ubuntu устранение неполадок администрирование Linux