Сервер начинает тормозить под нагрузкой, в логах появляется знакомая строка: WARNING: [pool www] server reached pm.max_children setting. Первый рефлекс - взять число, умножить его на два, перезапустить PHP-FPM. Нагрузка растёт ещё немного - и всё повторяется. Этот цикл можно прервать только одним способом: разобраться, как именно PHP-FPM управляет процессами и от чего на самом деле зависит правильное значение каждого параметра.
Здесь не будет магических формул с множителями "на количество ядер CPU". Будет методика, которая работает на конкретном железе с конкретным приложением.
Три режима управления процессами и когда применять каждый
PHP-FPM предлагает три принципиально разных подхода к управлению пулом: static, dynamic и ondemand. Выбор между ними - это первое решение, которое нужно принять до того, как трогать любые числовые параметры.
Режим static держит ровно pm.max_children процессов запущенными всегда. Никакого порождения и завершения под нагрузкой - процессы сидят в памяти и ждут запросов. Это самый быстрый вариант при пиковой нагрузке: когда приходит запрос, он немедленно попадает к уже готовому воркеру без задержки форка. На серверах с достаточным объёмом RAM и стабильным трафиком - правильный выбор для продакшена. Оборотная сторона очевидна: все pm.max_children процессов держат память даже когда сервер обслуживает один запрос в минуту.
Режим dynamic - дефолт большинства дистрибутивов. Он порождает процессы при необходимости и завершает их при простое, удерживая число между pm.min_spare_servers и pm.max_spare_servers. Кажется разумным компромиссом, но у него есть скрытая стоимость: форк нового процесса занимает несколько миллисекунд, и при резких всплесках трафика эта задержка накапливается. Практические тесты показывают, что при 10 000 запросов с concurrency 100 static выигрывает у dynamic около 24 миллисекунд суммарно - немного, но существенно если приложение само по себе быстрое.
Режим ondemand не держит никаких процессов в простое. Когда запрос приходит - форкается процесс, обрабатывает запрос, завершается через pm.process_idle_timeout секунд бездействия. Это идеальный выбор для shared-хостинга с сотнями пулов, где большинство сайтов получают единичные запросы в день - экономия памяти там огромная. Для высоконагруженного продакшен-сайта ondemand - худший вариант из трёх.
Базовая конфигурация для static на высоконагруженном сервере:
pm = static
pm.max_children = 50
pm.max_requests = 500
Как измерить реальный расход памяти одного процесса
Единственная правильная отправная точка для расчёта pm.max_children - это фактический расход памяти одного PHP-FPM процесса на своём приложении. Не среднее по интернету, не предположение "обычно около 50 МБ". Своё приложение.
Замерить расход памяти при живом трафике можно несколькими способами. Самый надёжный - через RSS (Resident Set Size) из системных данных о процессах:
# Получить PID мастер-процесса PHP-FPM
pgrep -o php-fpm
# Посмотреть память всех дочерних процессов (подставить свою версию PHP)
ps -C php-fpm8.3 --no-headers -o pid,rss | awk '{sum+=$2; n++} END {print sum/n/1024 " MB average, " n " processes"}'
Второй способ - через смапированную память, которая точнее учитывает разделяемые страницы:
# Суммарная и средняя память с учётом shared pages
sudo python3 /usr/lib/python3/dist-packages/ps_mem.py | grep php-fpm
Смотреть на эти числа нужно после нескольких часов работы под реальной нагрузкой, а не сразу после запуска. OPcache, realpath cache, session-данные - всё это со временем занимает своё место в памяти процесса. Свежезапущенный PHP-FPM занимает одни цифры, тот же процесс после тысячи запросов - другие, и разница может быть значительной. Типичные значения варьируются широко: голый PHP с простым приложением - 20-30 МБ, WordPress с плагинами - 60-80 МБ, тяжёлый Magento или Symfony с большим числом сервисов - 100-150 МБ и выше.
Расчёт pm.max_children по памяти сервера
Когда средний расход памяти одного процесса известен, расчёт прямой. Нужно взять объём RAM, доступный для PHP-FPM, разделить на средний размер процесса и умножить на коэффициент безопасности 0.75-0.80.
Доступная RAM для PHP-FPM - это не весь объём памяти сервера. Нужно вычесть:
- операционную систему и системные процессы (обычно 300-500 МБ),
- базу данных, если она на том же сервере (MySQL/MariaDB в типичной конфигурации - 1-4 ГБ),
- Redis, Memcached и другие сервисы,
- буфер для OPcache (256-512 МБ при стандартных настройках).
Скрипт для автоматического расчёта:
#!/bin/bash
TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}')
OS_RESERVED_MB=500
DB_RESERVED_MB=1024 # Скорректировать под реальный расход БД
OPCACHE_MB=256
PHP_AVAILABLE_MB=$((TOTAL_RAM_MB - OS_RESERVED_MB - DB_RESERVED_MB - OPCACHE_MB))
# Средний RSS одного процесса в МБ
AVG_PROCESS_MB=$(ps -C php-fpm8.3 --no-headers -o rss | \
awk '{sum+=$1; n++} END {printf "%.0f", sum/n/1024}')
MAX_CHILDREN=$(echo "scale=0; $PHP_AVAILABLE_MB * 0.80 / $AVG_PROCESS_MB" | bc)
echo "Доступно для PHP-FPM: ${PHP_AVAILABLE_MB} МБ"
echo "Средний размер процесса: ${AVG_PROCESS_MB} МБ"
echo "Рекомендуемое pm.max_children: ${MAX_CHILDREN}"
На сервере с 16 ГБ RAM, где MySQL занимает 2 ГБ, а средний PHP-процесс - 80 МБ, результат будет около 120 процессов. На VPS с 4 ГБ и тем же приложением - около 25-30.
Как правильно выставить pm.start_servers и spare-параметры
Параметр pm.start_servers актуален только для режима dynamic - он определяет, сколько процессов создаётся при старте PHP-FPM. Есть одно ограничение, которое документация упоминает вскользь: значение pm.start_servers обязано быть не меньше pm.min_spare_servers и не больше pm.max_spare_servers. Иначе PHP-FPM тихо скорректирует значение по своим правилам.
Широко распространённая формула "start_servers = CPU cores × 4" имеет сомнительное происхождение и ненадёжно работает на практике. Разумная отправная точка - выставить start_servers равным min_spare_servers, а min_spare_servers сделать равным 25% от max_children. Тогда max_spare_servers - 50-75% от max_children. Это даёт достаточно процессов для немедленного ответа без перегрева сервера при старте.
Конфигурация для dynamic на сервере с расчётным max_children = 80:
pm = dynamic
pm.max_children = 80
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 60
pm.max_requests = 500
Параметр pm.max_requests при этом важен независимо от режима: он перезапускает процессы после заданного числа обработанных запросов. Это страховка от постепенных утечек памяти в стороннем коде, которые иначе обнаруживаются через несколько дней по потреблению RAM. Значение 500-1000 работает для большинства приложений.
Разные пулы для разных задач
Одна из самых недооценённых возможностей PHP-FPM - раздельные пулы для разных частей приложения. Интернет-магазин, например, состоит из публичного фронтенда и административной панели. Их характеристики принципиально разные: фронтенд получает тысячи запросов в минуту от анонимных пользователей, бэкенд - единицы запросов от нескольких администраторов.
Держать одинаковые настройки для обоих - значит либо расходовать лишнюю память на админку, либо недодавать ресурсов магазину. Решение - два пула с разными режимами:
; /etc/php/8.3/fpm/pool.d/frontend.conf
[frontend]
listen = /run/php/php8.3-fpm-frontend.sock
pm = static
pm.max_children = 60
pm.max_requests = 500
; /etc/php/8.3/fpm/pool.d/backend.conf
[backend]
listen = /run/php/php8.3-fpm-backend.sock
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
pm.max_requests = 200
Разные сокеты - разные блоки upstream в Nginx, и каждая часть сайта получает ровно те ресурсы, которых требует её трафик.
Мониторинг через status-страницу и медленные запросы
Расчёт - это только начало. Реальные параметры неизбежно потребуют коррекции после нескольких дней работы под продакшен-нагрузкой. Для этого нужно знать, что именно мониторить.
PHP-FPM отдаёт текущее состояние пула через встроенную status-страницу. Включить её в конфигурации пула:
pm.status_path = /fpm-status
ping.path = /fpm-ping
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s
И ограничить доступ в Nginx только для локальных запросов:
location /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Ключевые метрики, за которыми нужно следить: active processes (если постоянно близко к max_children - нужно поднимать лимит), listen queue (очередь запросов, ожидающих свободного воркера, значение больше нуля - сигнал тревоги), max active processes (исторический максимум одновременно активных процессов с момента запуска).
Просмотреть все метрики одной командой:
curl -s http://127.0.0.1/fpm-status | grep -E 'active|idle|queue|max'
Slowlog фиксирует каждый запрос, который выполнялся дольше request_slowlog_timeout секунд, с полным стектрейсом PHP. Это ценнее, чем кажется: медленные запросы блокируют воркеры, и именно они чаще всего становятся причиной "server reached pm.max_children" - не потому что запросов слишком много, а потому что каждый из них занимает воркер дольше обычного.
Правильная настройка PHP-FPM не бывает окончательной. Приложение меняется, трафик меняется, железо обновляется. Но если расчёт сделан по реальным замерам памяти, режим управления процессами выбран под характер нагрузки, а мониторинг настроен с первого дня - система сама подскажет, когда и в какую сторону крутить параметры.
Первая неделя в продакшене обычно выявляет то, что не видно при тестировании: неожиданно тяжёлые CRON-задачи, пики трафика в определённые часы, специфику работы OPcache при перезапуске. Именно поэтому slowlog и status-страница должны быть настроены ещё до первого запуска под нагрузку, а не после первого инцидента. Система, за которой наблюдают с самого начала, ведёт себя предсказуемо - и оставляет время на архитектурные решения вместо пожарного тюнинга.