PHP-FPM под нагрузкой: что сломается первым
Сайт отдаёт 502 при трёх сотнях одновременных запросов, а на графиках CPU свободен наполовину. В девяти случаях из десяти дело в настройках пула PHP-FPM. Разбираем, где именно рвётся и что мерить.

Типичный сценарий: WordPress или самописный API держит нагрузку в будни, но в пик отдаёт 502 Bad Gateway, хотя top показывает загрузку CPU в 40–50%. Nginx в логах пишет connect() to unix:/run/php/php-fpm.sock failed (11: Resource temporarily unavailable) или upstream sent too big header. Память не кончилась, диск не забит, база отвечает. Проблема почти всегда в том, как настроен пул воркеров PHP-FPM и что происходит, когда все воркеры заняты.
Разбираем на PHP-FPM из PHP 8.2–8.3 (поведение менеджера процессов не менялось с 7.x, отличаются только дефолты в дистрибутивных пакетах). Смотрим только на то, что ломается под нагрузкой: исчерпание воркеров, очередь бэклога, утечки памяти в долгоживущих процессах и таймауты. Настройка OPcache и профилирование самого кода — отдельные темы.
Что происходит, когда воркеры кончились
PHP-FPM держит фиксированный или динамический набор процессов-воркеров. Каждый обрабатывает ровно один запрос за раз. Nginx через FastCGI передаёт запрос в сокет пула. Пока есть свободный воркер — запрос уходит ему. Когда свободных нет, запрос попадает в очередь ядра на сокете (backlog). Когда и очередь переполнена — ядро отклоняет соединение, Nginx получает отказ и отдаёт клиенту 502 или 504.
Ключевой момент: под нагрузкой вы упираетесь не в CPU, а в число одновременных воркеров. Триста параллельных запросов при pm.max_children = 50 означают, что 50 обрабатываются, часть стоит в бэклоге, остальные отбиваются. CPU при этом может простаивать, потому что воркеры ждут ответа от базы или внешнего API, а не считают.
PHP-FPM масштабируется по числу воркеров, а не по ядрам. Свободный процессор при 502-х — не парадокс, а типичная картина I/O-bound нагрузки.
Как посчитать max_children честно
Формула простая, но её часто игнорируют:
pm.max_children = (доступная память под PHP) / (память на один воркер)Память на воркер меряется, а не угадывается. Посмотрите реальное потребление живых процессов:
# RSS каждого воркера пула в мегабайтах
ps --no-headers -o rss,cmd -C php-fpm8.2 | awk '{sum+=$1; n++} END {print sum/n/1024 " MB avg, " n " workers"}'Если сервер отдал под PHP 4 ГБ, а средний воркер ест 60 МБ, потолок — примерно 4096 / 60 ≈ 68 воркеров. Ставить 200 «на всякий случай» опасно: при всплеске они одновременно займут память, начнётся своп, и сервер ляжет целиком вместо честных 502 от FPM.
Динамический pm: где ловушка
Режим pm = dynamic в дистрибутивных пакетах стоит по умолчанию с провокационно низкими цифрами. Пример дефолта из пакетов Debian/Ubuntu:
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3max_children = 5 означает, что шестой одновременный запрос уже встаёт в очередь. Для одностраничного лендинга нормально, для магазина в пик — гарантированный 502. Вторая ловушка: при dynamic FPM тратит время на старт и остановку процессов под колеблющейся нагрузкой, и в момент всплеска новые воркеры ещё не подняты.
static, dynamic, ondemand — что когда
| Режим | Когда подходит | Цена |
|---|---|---|
static | Стабильная высокая нагрузка, память посчитана | Всегда держит max_children процессов и всю их память, даже ночью |
dynamic | Нагрузка колеблется в течение суток | Задержка на подъём воркеров в момент всплеска |
ondemand | Много редко посещаемых сайтов на одном сервере | Холодный старт воркера на первом запросе после простоя |
Критерий выбора: если сервер обслуживает один нагруженный проект и память посчитана — static предсказуемее всего. Если на сервере три десятка мелких сайтов — ondemand экономит память. dynamic — компромисс по умолчанию, но требует вменяемых цифр вместо дефолтных пяти.
Бэклог и listen.backlog: тихий обрыв
Даже с правильным max_children всплеск может переполнить очередь на сокете. Параметр listen.backlog задаёт длину этой очереди. В PHP-FPM дефолт — 511, но реальный предел ограничен системным net.core.somaxconn, который в старых ядрах равен 128. Если somaxconn ниже listen.backlog, ядро молча обрежет очередь до системного значения, и вы будете думать, что бэклог большой, а он 128.
- Проверьте системный лимит:
sysctl net.core.somaxconn. - Если он ниже вашего
listen.backlog— поднимите системный, иначе значение в пуле бессмысленно. - Большой бэклог не лечит нехватку воркеров, он лишь превращает быстрый 502 в медленный 504. Запрос дождётся воркера, но клиент к тому моменту уже уйдёт по таймауту Nginx.
Долгоживущие воркеры и утечки памяти
Воркер обрабатывает много запросов подряд, не завершаясь. Если код или расширение течёт — память воркера растёт от запроса к запросу, пока процесс не съест лишнее и не уйдёт в своп. Спасает pm.max_requests: воркер перезапускается после N обработанных запросов, сбрасывая накопленную память.
# воркер перезапускается каждые 500 запросов
pm.max_requests = 500Слишком маленькое значение (например, 50) убьёт производительность: постоянные рестарты воркеров сжигают время на инициализацию. Слишком большое (0 — без ограничения) даст утечке расти бесконтрольно. Ориентир для течущего кода — 300–1000, для чистого можно оставить дефолтные пакетные значения.
Таймауты: где рвётся цепочка
Медленный запрос держит воркер занятым всё время выполнения. Один тяжёлый отчёт на 60 секунд — это один воркер, недоступный минуту. Тридцать таких запросов при max_children = 50 съедят больше половины пула. Ограничивайте время в двух местах:
request_terminate_timeoutв пуле FPM — жёстко убивает воркер, зависший дольше указанного времени, и пишет строку в slow log.fastcgi_read_timeoutв Nginx — сколько Nginx ждёт ответа от FPM. Если он меньшеrequest_terminate_timeout, клиент получит 504 раньше, чем FPM успеет завершить запрос.
Эти два таймаута должны быть согласованы, иначе диагностика превращается в гадание: Nginx уже отдал 504, а воркер ещё работает и держит ресурс.
Slow log — куда смотреть первым делом
Включите лог медленных запросов, он покажет стек PHP на момент зависания:
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.logПосле включения в логе появятся трассировки запросов, которые выполнялись дольше 5 секунд, с указанием функции и файла. Это точнее, чем гадать по метрикам: видно конкретное место, где воркер завис.
Что мерить в проде
PHP-FPM отдаёт статус пула через встроенную страницу. Включается в пуле директивой pm.status_path, читается через Nginx-локейшн, проксирующий этот путь в FPM. Ключевые поля ответа:
active processes— сколько воркеров занято прямо сейчас;max active processes— пиковое число занятых за время работы. Если оно упирается вmax_children— вы достигли потолка, пора поднимать лимит или искать медленные запросы;listen queue— сколько запросов ждёт в бэклоге. Ненулевое значение под нагрузкой — сигнал, что воркеров не хватает;max listen queue— пик очереди. Если он больше нуля — часть клиентов ждала или получила отказ.
Точный формат и полный список полей смотрите в официальной документации FPM — набор полей менялся между версиями PHP.
Что настроить дальше
Первый шаг — снять реальное потребление памяти на воркер и пересчитать max_children под фактический объём RAM, а не оставлять дистрибутивную пятёрку. Второй — подключить страницу статуса и завести на неё мониторинг с алертом на max active processes у потолка. Третий — включить slow log хотя бы на неделю: он покажет, лечится ли проблема добавлением воркеров или всё упирается в один медленный SQL-запрос, который поднятием лимитов не исправить.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: PHP: Configuration — FPM (официальная документация), PHP: FPM Status Page — официальная документация
Частые вопросы
У меня PHP 8.1, а не 8.3 — настройки те же?
Поднял max_children, а 502 не пропали — что дальше?
Какое значение max_children выбрать для сервера с 8 ГБ?
Что грозит, если оставить max_requests = 0?
Чем static отличается от dynamic на практике?
Почему listen.backlog не помогает при нехватке воркеров?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.