PHP-FPM под нагрузкой: настройка пулов без 502 и OOM
Nginx отдаёт 502, а сервер при этом почти не загружен — типичный симптом неправильного pm.max_children. Разбираем, как считать лимиты пула по памяти, а не наугад.

Классическая картина: сайт живёт на PHP-FPM, приходит всплеск трафика, и Nginx начинает отдавать 502 Bad Gateway. При этом htop показывает, что процессор простаивает, а память забита не под завязку. Проблема почти всегда не в железе, а в том, что пул FPM упёрся в pm.max_children и новым запросам некому отвечать. Ниже — как посчитать этот и соседние лимиты по памяти и характеру нагрузки, а не переписать из чужого конфига.
Материал для инженера, который настраивает PHP-FPM 8.x (проверялось на 8.1–8.3) за Nginx на Linux. Угол узкий: только параметры пула под нагрузку. Выбор между FPM и другими SAPI, тюнинг OPcache и настройка самого Nginx — отдельные темы, здесь их не трогаем.
Откуда берётся 502 при свободном CPU
PHP-FPM держит пул воркеров — отдельных процессов, каждый обрабатывает один запрос за раз. Nginx через fastcgi_pass кладёт запрос в сокет FPM. Если все воркеры заняты, а достигнут потолок pm.max_children, запросы встают в очередь listen.backlog. Переполнилась и она — FPM закрывает соединение, Nginx это видит как ошибку апстрима и отдаёт 502.
CPU при этом свободен, потому что воркеры не считают, а ждут: ответа базы, внешнего API, диска. То есть узкое место — не вычисления, а количество одновременных «ожидающих» процессов, которое вы сами ограничили.
502 при свободном процессоре почти никогда не лечится покупкой ядер. Лечится он честным расчётом max_children по памяти — и пониманием, что часть ожиданий надо убрать из PHP вовсе.
Считаем pm.max_children по памяти
Главный ограничитель — оперативная память. Каждый воркер занимает от нескольких десятков до пары сотен мегабайт в зависимости от кода и расширений. Формула проста:
max_children = (доступная под FPM память) / (средний размер одного воркера)Средний размер воркера смотрят так (замените имя процесса под вашу версию):
# реальное потребление одного пула, RSS в мегабайтах
ps --no-headers -o rss,cmd -C php-fpm8.2 | \
awk '/pool/ {sum+=$1; n++} END {printf "воркеров: %d, средний RSS: %.1f МБ\n", n, sum/n/1024}'Пример: сервер с 8 ГБ, из которых под ОС, Nginx, БД и кэш вы резервируете 4 ГБ. Под FPM остаётся 4 ГБ. Средний воркер — 80 МБ. Тогда потолок примерно 4096 / 80 ≈ 51 воркер. Округлите вниз, до 45–48, чтобы оставить запас на всплески размера воркера. Ставить 200 «на всякий случай» — прямой путь к OOM killer, который прибьёт FPM или, хуже, базу.
Как ведут себя режимы pm
Параметр pm задаёт стратегию управления воркерами. Три режима с разной ценой:
| Режим | Что делает | Цена |
|---|---|---|
static | Держит ровно pm.max_children процессов всегда | Предсказуемая память, нулевая задержка на старте воркера. Тратит RAM в простое |
dynamic | Держит от min_spare до max_spare, растёт до max_children | Экономит память в простое, но создание воркера под всплеском добавляет задержку |
ondemand | Стартует воркеры только под запрос, гасит после pm.process_idle_timeout | Минимум памяти в простое, максимальная задержка холодного старта. Хорош для редких сайтов на общем сервере |
Для нагруженного продакшена с ровным трафиком чаще берут static: память вы всё равно посчитали по потолку, а лишних задержек на форк процессов нет. Для сервера с десятком сайтов и неравномерным трафиком — dynamic или ondemand.
Параметры пула, которые реально влияют
Файл пула обычно лежит в /etc/php/8.2/fpm/pool.d/www.conf (путь зависит от версии и дистрибутива). Ключевые строки для dynamic:
[www]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm.sock
pm = dynamic
pm.max_children = 48
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.max_requests = 500
; очередь ожидающих соединений сверх занятых воркеров
listen.backlog = 511
; медленные запросы — в отдельный лог
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
; страховка от зависших запросов
request_terminate_timeout = 30sЧто здесь важно понимать:
- pm.max_requests — воркер перезапускается после N запросов. Спасает от утечек памяти в сторонних расширениях. Значение 0 отключает перезапуск; для кода с известными утечками ставьте 300–1000.
- request_terminate_timeout должен быть больше вашего самого долгого легитимного запроса, но конечным. Иначе один зависший на внешнем API воркер держит слот до бесконечности и приближает исчерпание пула.
- listen.backlog — размер очереди на сокете. Больше очередь — меньше 502 под коротким всплеском, но выше задержка для стоящих в очереди. Не лечит хроническую нехватку воркеров, только сглаживает пики.
Разделяйте пулы под разную нагрузку
Не гоните весь трафик через один пул. Тяжёлый эндпоинт (экспорт, генерация PDF, вызов медленного API) занимает воркер надолго и вытесняет быстрые запросы. Заведите отдельный пул со своим сокетом и своим лимитом и направьте на него только тяжёлые пути через отдельный fastcgi_pass в Nginx. Тогда зависшая выгрузка не уронит выдачу главной страницы.
Замеряем, а не гадаем
Включите страницу статуса пула — добавьте в конфиг пула:
pm.status_path = /fpm-statusИ отдайте её только локально в Nginx, чтобы не светить наружу. Тогда доступны реальные числа под нагрузкой:
# листенер-очередь и активные процессы прямо сейчас
curl -s http://127.0.0.1/fpm-status?full | grep -E 'listen queue|active processes|max active'Смотрите на два показателя:
- listen queue — если стабильно больше нуля, воркеров не хватает: запросы ждут. Поднимайте
max_children, если позволяет память, иначе оптимизируйте сами запросы. - max active processes — пиковое число занятых воркеров с момента старта. Если оно упирается в
max_children, вы достигли потолка под нагрузкой.
Замеры делайте под реальным или нагрузочным трафиком (ab, wrk, k6), а не в простое. Условия фиксируйте: сколько виртуальных пользователей, какой эндпоинт, какое железо. Число max_children, подобранное на пустом сервере, под боем окажется бесполезным.
Где это ломается
- OOM killer. Поставили
max_childrenс запасом «на будущее», трафик пришёл — суммарная память воркеров превысила RAM, ядро прибило случайный процесс. Всегда считайте потолок по памяти, а не по желаемому числу запросов. - Воркеры распухают. Средний RSS вы замерили на лёгких запросах, а тяжёлый отчёт грузит в память гигабайт. Один такой воркер ломает весь расчёт. Отсюда — отдельный пул и
memory_limitна уровне PHP. - Backlog маскирует проблему. Раздули
listen.backlogдо тысяч — 502 исчезли, но пользователи ждут ответа секундами. Очередь не заменяет воркеры, она лишь откладывает отказ. - request_terminate_timeout = 0. Один воркер завис на неотвечающем внешнем сервисе и держит слот навсегда. Под нагрузкой такие слоты копятся, пул исчерпывается — снова 502.
Что делать дальше
Порядок действий на вашем сервере: замерьте реальный RSS воркера под нагрузкой, посчитайте max_children по свободной памяти, включите pm.status_path и понаблюдайте listen queue и max active processes под нагрузочным тестом. Дальше смотрите на медленный лог: если в нём висят одни и те же запросы, узкое место в коде и базе, а не в размере пула — тюнинг FPM его не спрячет. Следующий шаг после стабилизации пула — OPcache и профилирование самих запросов, это отдельные темы.
Источники: PHP: FastCGI Process Manager (FPM) — Configuration, PHP: List of global php-fpm.conf directives
Частые вопросы
У меня PHP-FPM 7.4, а не 8.x — конфиг подойдёт?
Поднял max_children, но 502 не ушли. Что не так?
Какой режим pm выбрать — static или dynamic?
Как понять, что max_children поставлен слишком высоко?
Нужно ли перезапускать FPM после правки конфига?
Насколько это заменяет тюнинг Nginx и базы данных?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.