Вебформат

Главная/Блог/ИТ-специалистам

ИТ-специалистам21 августа 20267 мин

Сервер не нагружен, а сайт отдаёт 504: как найти зависшие рабочие процессы PHP

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

504 при нулевой нагрузке · зависшие процессы PHP
504 при нулевой нагрузке · зависшие процессы PHP

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

Дальше можно было гадать и перезагружать всё подряд. Вместо этого мы прошли по слоям сверху вниз - от того, что видно снаружи, к тому, что происходит внутри. Диагностика заняла минуты. Ниже - тот же порядок действий, он повторяем на любом сайте на PHP за веб-сервером.

Слой первый: что видно снаружи

Первое измерение делается с чужого компьютера, без доступа к серверу. Нужно сравнить две вещи: как отдаётся статический файл и как отдаётся любой адрес, который обрабатывает PHP.

curl -o /dev/null -s -w "%{http_code} %{time_total}\n" https://site.ru/robots.txt
curl -o /dev/null -s -w "%{http_code} %{time_total}\n" https://site.ru/login
Статика и страница с PHP - двумя запросами подряд

У нас статика отдавалась за полсекунды, а любой адрес с PHP висел ровно 60 секунд и заканчивался кодом 504. Это уже диагноз половины проблемы: веб-сервер жив, отдаёт файлы и держит соединение. Молчит то, что стоит за ним.

Ровно 60 секунд - не совпадение. Столько nginx по умолчанию ждёт ответа от обработчика PHP (параметр fastcgi_read_timeout), после чего отвечает посетителю сам. Если в вашем случае страница висит 30 или 300 секунд - значит этот параметр в конфигурации меняли, а суть та же.

502 и 504 - разные поломки и разное лечение

Эти два кода часто считают синонимами "сайт лежит". На деле они говорят о противоположных вещах, и путать их дорого: ищешь не там.

502 Bad Gateway504 Gateway Timeout
Что произошлоОбработчик PHP оборвал соединение или его нет на местеОбработчик соединение принял, но ответа не прислал
Частая причинаПроцесс упал, кончилась память, сервис не запущенВсе рабочие процессы заняты, запрос ждёт очереди
Как быстроОтвет приходит сразуОтвет приходит после ожидания веб-сервера
Где смотретьЖурнал ошибок PHP, служба и её автозапускЗанятость рабочих процессов и очередь на сокете
Один симптом "сайт не открывается", два разных места поломки

У нас был устойчивый 504 после минуты ожидания. Значит служба на месте, соединения принимает и молчит - и искать надо не в автозапуске, а в том, чем заняты её рабочие процессы.

Слой второй: одна строка, которая ставит диагноз

Самое полезное измерение внутри сервера - очередь на слушающем сокете обработчика PHP. Одна команда:

netstat -ltnp | grep 9000
# tcp  129  128  0.0.0.0:9000  0.0.0.0:*  LISTEN  1/php-fpm: master
Первое число - очередь непринятых соединений

У слушающего сокета первое число (Recv-Q) - это соединения, которые уже пришли, но которые никто не забрал в работу. Второе (Send-Q) - максимальный размер этой очереди. В норме первое число - ноль: запросы разбирают быстрее, чем они приходят. У нас там стояло 129 при пределе 128, то есть очередь была забита полностью и переполнена.

Следующая команда показывает, чем именно заняты процессы: список их соединений. У нас каждый из восьми рабочих процессов держал открытое исходящее соединение на 443-й порт к чужим серверам - то есть сидел и ждал ответа на запрос, ушедший наружу. Ответа не было. Рядом висели старые связки с веб-сервером в состоянии CLOSE_WAIT: тот уже отвалился по таймауту и закрыл свою сторону, а процесс этого даже не заметил - он всё ещё ждал сеть.

Как один молчащий чужой сервер кладёт весь сайт
Внешний сервер не отвечает
Запрос ушёл, ответа нет и не будет
Ограничения по времени в коде нет
процесс ждёт сеть и новых запросов не берёт
Рабочий процесс занят навсегда
Ожидание сети не считается временем работы скрипта
Штатный предел времени выполнения его не снимает
то же повторяется с остальными
Заняты все процессы пула
Свободных рабочих мест не осталось
8 из 8
новые соединения принимать некому
Очередь на сокете переполнена
Запросы стоят и ждут своей очереди
129
веб-сервер ждёт 60 секунд и сдаётся
504 у каждого посетителя
Статика при этом открывается за полсекунды
Нагрузка на сервере нулевая
Сервер не перегружен - он простаивает. Заняты не ресурсы, а рабочие места.

Почему предел времени выполнения тут не спасает

Обычное возражение: у PHP же есть ограничение на время работы скрипта, тридцать секунд по умолчанию. Почему процесс висит сутки?

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

Усугубляет то, что у популярных способов сходить наружу ограничения по времени по умолчанию нет вовсе: у cURL время ответа не ограничено, пока его не задали явно. Один невыставленный параметр в коде интеграции - и однажды весь сайт ложится из-за чужого сервера, к которому этот код обращается.

Слой третий: задачи по расписанию, которые плодятся

Рядом обнаружился второй источник зависших процессов. Соседнее приложение на том же сервере запускало консольную задачу по расписанию - каждую минуту. Задача обращалась к тем же молчащим адресам и не завершалась. Планировщик про это не знал и честно запускал следующую копию.

На момент разбора висело девять таких копий возрастом от 20 до 22 часов. Каждая держала соединение и занимала память. Само по себе это сайт не роняет, но забирает ресурсы и заметно мешает разбираться: список процессов превращается в кашу из одинаковых строк.

Как снимать зависшие процессы

Лечение симптома простое: снять зависшие процессы. Управляющий процесс поднимет свежие сам, перезапускать службу целиком и тем более сервер не нужно. У нас после этого очередь на сокете обнулилась, и сайт стал отвечать кодом 200 за полсекунды вместо 504 за минуту.

# сначала посмотреть, кого именно поймает шаблон
pgrep -af "app:swap-[r]esponsible"

# и только потом снимать
pkill -f "app:swap-[r]esponsible"
Сначала список, потом снятие - порядок, который экономит нервы

Отдельно стоит проверить, что перезапуск вообще безопасен. На сервере, где живёт несколько сайтов, у каждого свой пул процессов - и перезапуск "всего PHP" уронит соседей. Мы такие ограничения держим записанными: где какой контейнер, что перезапускать нельзя и что после перезагрузки сервера может не подняться само.

Что чинить, чтобы это не повторилось

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

  • 01Ограничение по времени на каждый исходящий запрос в коде. Главная мера. Секунды на соединение, десятки секунд на ответ. Не ответили - обрабатываем как ошибку, а не ждём вечно.
  • 02Аварийный предел на длительность запроса в настройках PHP-FPM (параметр request_terminate_timeout). Страховка на случай, когда штатное ограничение по времени не сработало: процесс снимается принудительно, рабочее место возвращается в пул.
  • 03Запрет задачам по расписанию накладываться друг на друга. Блокировка перед запуском - и девять копий одной задачи больше не соберутся.
  • 04Проверка снаружи по настоящей странице, а не по доступности сервера. Пинг и графики загрузки такой сбой не видят - у них всё зелёное.

Последний пункт стоит отдельного абзаца, потому что именно он определяет, узнаете вы о простое через минуту или через сутки. Проверка вида "порт открыт" и "нагрузка в норме" в нашем случае показывала бы идеальную картину все двадцать четыре часа. Единственная проверка, которая ловит такой сбой, - запрос настоящей страницы с PHP со стороны, с ограничением по времени: не ответила за столько-то секунд - тревога.

Памятка: пять шагов при 504

  • 01Сравнить снаружи статический файл и страницу с PHP. Статика быстрая, PHP висит - веб-сервер жив, дело за ним.
  • 02Посмотреть очередь на сокете обработчика. Больше нуля - процессы заняты. Ноль - смотреть, запущена ли служба.
  • 03Посмотреть соединения занятых процессов. Исходящие наружу - ищите интеграцию без ограничения по времени.
  • 04Снять зависшие процессы шаблоном с квадратными скобками, проверив список заранее.
  • 05Закрыть причину: время ожидания в коде, аварийный предел в настройках, блокировка задач по расписанию, проверка настоящей страницы снаружи.

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

Сайт лежит, а причину никто не находит?

Обсудим ваш проект - поднимем сервис, найдём причину простоя и закроем её, чтобы он не лёг снова через неделю.