Ночью сайт перестал открываться: ошибка 502 у всех посетителей, включая рекламный трафик. Сервер при этом был здоров — низкая нагрузка, свободная память, база отвечает. Сайт пролежал 5 часов 40 минут и сам не поднялся бы никогда. Причина — одна строка, которой не было в настройках пула PHP-FPM.
Это вторая часть цикла «Сервер для WordPress». Случай произошёл на сайте не на WordPress, но пул PHP-FPM у всех одинаковый, и та же настройка отсутствовала на всех наших сайтах.
Что случилось
- модуль сайта на каждом запросе делал исходящий HTTP-запрос к внешнему сервису — без таймаута на чтение;
- внешний сервер принял соединение и замолчал. Не отказал, не оборвал — просто перестал отвечать;
- каждый процесс PHP, дошедший до этого места, повис в ожидании ответа навсегда;
- через несколько минут повисли все 100 процессов пула. Новым запросам некуда было идти — nginx отвечал 502.
В журнале nginx это выглядело так:
connect() to unix:/var/run/php/php-fpm.sock failed (11: Resource temporarily unavailable)
Подняли сайт перезапуском контейнера PHP. Без перезапуска процессы ждали бы ответа, который не придёт.
Почему умерли все 100, а не часть
Сеть держала около 15 процессов. Остальные повисли из-за усилителя — файловых сессий. Зависший процесс держит блокировку на своём файле сессии, и все следующие запросы того же посетителя встают за ней в очередь. На одном файле сессии стояли 27 процессов, на другом — 22.
Предохранитель, которого не было
В PHP-FPM есть параметр, который убивает процесс, если запрос длится дольше заданного времени:
request_terminate_timeout = 300s
По умолчанию он равен нулю — без ограничения. И max_execution_time тут не помогает: время ожидания сети в него не засчитывается, а в нашем случае и он стоял в 0.
С предохранителем та же авария выглядела бы как пачка ошибок 504 у тех, кто попал в зависание, — и работающий сайт для остальных. «Лежим до утра» превращается в «несколько ошибок и живём дальше».
Какое значение ставить. Больше самого долгого нормального запроса сайта. Для страниц и админки хватает и 60 секунд; у нас стоит 300 — с запасом на выгрузки и обмен с 1С. Если есть очень долгие импорты по HTTP, им нужен отдельный пул, а не общий большой лимит.
Куда это вписать в Docker
В нашем образе основной конфиг подключает все файлы из каталога расширений, поэтому настройку не нужно вписывать в чужой www.conf. Отдельный файл с именем, которое идёт последним по алфавиту, перебивает значения:
php:
volumes:
- ./php/fpm/zz-pool.conf:/etc/php-fpm/zz-pool.conf:ro
; zz-pool.conf
[www]
request_terminate_timeout = 300s
request_slowlog_timeout = 5s
slowlog = /proc/self/fd/2
request_slowlog_trace_depth = 40
Применяется пересозданием контейнера PHP — простой в секунды.
Как распознать такую аварию за пять минут
- сайт отдаёт 502, а сервер здоров: нагрузка низкая, память и диск в норме, база отвечает;
- в журнале nginx —
connect() to unix:…php-fpm.sock failed (11: Resource temporarily unavailable); - процессов пула ровно
pm.max_children, все спят с почти нулевым временем процессора и одинаковым временем старта — это момент аварии:ps -eo pid,etime,time,args | grep "pool www"; - главная улика — slowlog: в нём готовые стеки зависших запросов с точной строкой кода и адресом, куда шёл запрос;
- куда висят соединения —
/proc/net/tcpвнутри контейнера иls -l /proc/<pid>/fd.
Мораль
Любой исходящий вызов на горячем пути должен иметь таймаут, а пул — предохранитель.
Таймауты в коде правильнее, но их надо найти все, включая чужие модули. Предохранитель в пуле ставится одной строкой и защищает от всех сразу. Поэтому сначала он.
Сайт иногда «ложится» с 502 и поднимается только перезапуском?
Пришлите адрес — проверю пул PHP-FPM и найду, что именно в нём зависает.
// загружаю комментарии…