Ночью сайт перестал открываться: ошибка 502 у всех посетителей, включая рекламный трафик. Сервер при этом был здоров — низкая нагрузка, свободная память, база отвечает. Сайт пролежал 5 часов 40 минут и сам не поднялся бы никогда. Причина — одна строка, которой не было в настройках пула PHP-FPM.

Это вторая часть цикла «Сервер для WordPress». Случай произошёл на сайте не на WordPress, но пул PHP-FPM у всех одинаковый, и та же настройка отсутствовала на всех наших сайтах.

Что случилось

  1. модуль сайта на каждом запросе делал исходящий HTTP-запрос к внешнему сервису — без таймаута на чтение;
  2. внешний сервер принял соединение и замолчал. Не отказал, не оборвал — просто перестал отвечать;
  3. каждый процесс PHP, дошедший до этого места, повис в ожидании ответа навсегда;
  4. через несколько минут повисли все 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 — простой в секунды.

Как распознать такую аварию за пять минут

  1. сайт отдаёт 502, а сервер здоров: нагрузка низкая, память и диск в норме, база отвечает;
  2. в журнале nginx — connect() to unix:…php-fpm.sock failed (11: Resource temporarily unavailable);
  3. процессов пула ровно pm.max_children, все спят с почти нулевым временем процессора и одинаковым временем старта — это момент аварии: ps -eo pid,etime,time,args | grep "pool www";
  4. главная улика — slowlog: в нём готовые стеки зависших запросов с точной строкой кода и адресом, куда шёл запрос;
  5. куда висят соединения — /proc/net/tcp внутри контейнера и ls -l /proc/<pid>/fd.

Мораль

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

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

Сайт иногда «ложится» с 502 и поднимается только перезапуском?

Пришлите адрес — проверю пул PHP-FPM и найду, что именно в нём зависает.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.