Про сбой сообщил пользователь, а не мониторинг: приложение не открывается, сервер отдаёт 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 висел ровно 60 секунд и заканчивался кодом 504. Это уже диагноз половины проблемы: веб-сервер жив, отдаёт файлы и держит соединение. Молчит то, что стоит за ним.
Ровно 60 секунд - не совпадение. Столько nginx по умолчанию ждёт ответа от обработчика PHP (параметр fastcgi_read_timeout), после чего отвечает посетителю сам. Если в вашем случае страница висит 30 или 300 секунд - значит этот параметр в конфигурации меняли, а суть та же.
502 и 504 - разные поломки и разное лечение
Эти два кода часто считают синонимами "сайт лежит". На деле они говорят о противоположных вещах, и путать их дорого: ищешь не там.
| 502 Bad Gateway | 504 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: тот уже отвалился по таймауту и закрыл свою сторону, а процесс этого даже не заметил - он всё ещё ждал сеть.
Почему предел времени выполнения тут не спасает
Обычное возражение: у 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Закрыть причину: время ожидания в коде, аварийный предел в настройках, блокировка задач по расписанию, проверка настоящей страницы снаружи.
Сбой из этого разбора выглядел как отказ сервера, а был отказом одного чужого адреса, к которому обращался код. Между тем, что замолчал чужой сервер, и тем, что лёг ваш сайт, стоит ровно одна строчка настройки - ограничение по времени на исходящий запрос. Пока её нет, доступность вашего сайта равна доступности самого медленного сервиса, к которому он ходит.



