Предупреждение php-fpm max_children: найдите причину в логах, прежде чем добавлять воркеры

Предупреждение php-fpm max_children означает, что все рабочие процессы PHP заняты обработкой запросов и новые запросы попадают в очередь. На сервере WordPress с Nginx, Apache и php-fpm, где это предупреждение копилось неделями, по логам были выявлены запросы, занимавшие воркеры, и проблему решило ограничение частоты запросов в nginx, а не повышение значения настройки.
При еженедельной проверке предупреждение фиксировалось 27–187 раз в день, но за те же четыре дня посетители не получили ни одной ошибки 502·504. Воркеры занимали не посетители, а бот, раз за разом запрашивавший адреса админки, и сканер, запрашивавший несуществующие пути.
1️⃣ php-fpm max_children & seems busy
Когда за день набирается 66 предупреждений, легко решить, что воркеров не хватило 66 раз. В логе php-fpm смешаны два разных по сути предупреждения, и большинство из них — предварительные, записанные ещё до достижения лимита.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] сообщает, что свободных воркеров стало 0 и создаётся новый процесс; при способе управления процессами dynamic это записывается даже при кратком росте запросов. [server reached pm.max_children] — момент, когда достигнут лимит в 26 процессов и создать новые нельзя; с этого момента новые запросы ждут.
Это как разница между тем, чтобы позвать в ресторане ещё одного официанта, и тем, чтобы оставить гостей ждать на улице, потому что все места заняты.
Подсчёт за один день по типам дал 58 seems busy и 6 max_children. Регулярная проверка сложила оба предупреждения и сообщила о 66, но на самом деле лимит был достигнут шесть раз.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44Шесть отметок времени, которые выводит вторая команда, становятся опорой для поиска причины; на следующем шаге используются только они.
2️⃣ Почему увеличение max_children — не решение
Если поднять лимит 26, предупреждений может стать меньше, но это не изменит того, какие запросы занимали воркеры.
Обычный расчёт — разделить доступную память на средний объём памяти одного воркера. На измеренном сервере 26 воркеров использовали в среднем 131MB и максимум 214MB, и если считать по оставшейся доступной памяти, на бумаге запас для новых воркеров был.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -mЧетыре значения в первом выводе — число воркеров, средняя память (MB), максимальная память (MB) и сумма (MB); столбец available в выводе free — это доступная память.
Если воркеров больше, чем ядер CPU, увеличение их числа не добавляет одновременно обрабатываемых запросов, а только удлиняет время обработки каждого. Пока посетители получают 0 ошибок 502·504, повышение лимита ничего не даёт и лишь отдаёт больше процессов ботам и сканерам, которые и так занимали воркеры.
Перед повышением лимита нужно проверить, были ли запросы, занимавшие воркеры в моменты предупреждений, посетителями; порядок проверки по access.log описан в следующем разделе.
3️⃣ Поиск причины в access.log: время, IP, путь, код ответа
Выберите одну отметку предупреждения — ответ будет в access.log за эти 1–2 минуты. Порядок такой: извлечь строки по времени, подсчитать по IP, затем проверить пути и коды ответа у первых IP.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 4049 октября в 18:09 за одну минуту было записано 286 запросов; 115 из них пришли с одного IP, пути были адресами админки вроде /wp-admin/profile.php, а код ответа везде — 302.
Код ответа показывает, дошёл ли запрос до PHP. 200 для кешированного HTML, который nginx отдаёт сам, а также 444·400 обходятся без PHP, а 302·404 и некешированный 200 занимают по одному воркеру. Когда адрес админки запрашивают без входа, WordPress отвечает 302 на страницу входа, и каждый такой 302 — это один запуск PHP.
Если на одном сервере два сайта и больше, строки обоих сайтов записываются вместе в общий access.log. Сначала казалось, что бот атакует первый сайт, но сверка с логами каждого сайта показала, что 22 390 из 22 391 запросов были ко второму сайту.
4️⃣ Три вида запросов, занявших воркеры: wp-admin от бота, 404 от сканера, предзагрузка кеша
Когда тот же порядок применили ко всем моментам предупреждений, оказалось, что воркеры занимали запросы трёх видов.
Первый — бот, раз за разом запрашивавший адреса админки. Один IP отправил 22 391 запрос за три часа; 9 944 из них пришлись на четыре пути, включая /wp-admin/profile.php, а поскольку каждый 302 без входа — это один запуск PHP, воркер использовался 9 944 раза.
Второй — сканер уязвимостей, запрашивавший несуществующие пути. Даже если в User-Agent написано Googlebot, это не Googlebot: IP, отправлявший 14 672 запроса в день, был виртуальным сервером Google Cloud с обратным DNS на googleusercontent.com, а у Googlebot он оканчивается на googlebot.com. Тот же IP по очереди представлялся как bingbot, Applebot, GPTBot и ClaudeBot.
Запросы к файлам вроде .env nginx обрывает кодом 444 за 0,04 секунды, но на обычные пути, которых нет в правилах блокировки, например /userfiles или /secrets.yml, WordPress генерирует страницу 404. Такая страница весит 266KB и требует 1,4–2,9 секунды — столько же, сколько генерация обычной записи, а один IP запрашивал её 4 069 раз в день.
Третий — сам сервер. При сохранении записи или перевода кеш очищается, и предзагрузка WP Rocket заново запрашивает все URL — 8 489 запросов в день, до 60 в минуту. Проверка статуса индексации Rank Math через Action Scheduler тоже занимала один воркер 754 раза в день. Оба процесса приходились только на рабочие часы и не вызывали ошибок у посетителей, поэтому их исключили из мер.
В симуляторе ниже видно, на каком уровне заканчивается каждый запрос и занимает ли он воркер PHP.
| Запрос | Где останавливается | Ответ | Воркеры |
|---|---|---|---|
| wp-admin от бота | WordPress | 302 (на страницу входа) | Используется 1 |
| Сканер, файлы типа .env | Правило блокировки nginx | 444 (0,04 с) | Не используется |
| Сканер, обычный фальшивый путь | WordPress | Страница 404 (266KB · 1,4–2,9 с) | Используется 1 |
| Предзагрузка кеша | WordPress | 200 · сохранение в файл кеша | Используется 1 |
| Визит на кешированную запись | Файл кеша | 200 (nginx отвечает сразу) | Не используется |
| Визит на некешированную запись | WordPress | 200 · сохранение в файл кеша | Используется 1 |
5️⃣ Ограничение частоты в nginx: лимит для запросов без входа и восстановление реального IP
Вместо блокировки выбрано ограничение частоты. Если заблокировать путь, сканер сменит путь; если заблокировать IP, на следующий день придёт другой IP. Ограничиваются только запросы без входа, доходящие до PHP: 30 в минуту и 60 одновременно на IP, а сверх лимита отдаётся 429.
Для сервера за CDN одной этой настройки недостаточно. nginx видит IP-адрес edge-узла CDN, и все посетители, идущие через этот узел, сливаются в один IP; сначала нужно прописать список IP edge-узлов в set_real_ip_from и восстановить реальный IP из X-Forwarded-For. Запросы с cookie входа исключаются из лимита, а по User-Agent исключений нет.
# контекст http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # список edge-узлов CDN
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location, передающий запросы в PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Кешированные страницы отдаются через try_files внутри nginx и до этого location не доходят, а admin-ajax.php обрабатывается в отдельном location и под лимит не попадает.
Проверять нужно параллельными запросами. Последовательные запросы не превышают 30 в минуту, потому что одна 404 занимает 1,5 секунды, и под лимит не попадают; если отправить 100 запросов по 20 одновременно, получится 67 ответов 404 и 33 ответа 429 — значит, лимит работает.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cЕсли в выводе есть 429, лимит работает. Если IP самого сервера исключён из лимита, на время проверки снимите исключение или запустите проверку из другой сети.
6️⃣ Сравнение до и после
Лимит на адреса админки без входа включили вечером 9 октября, а лимит на запросы, доходящие до PHP, — вечером 11-го. Число ответов 302 для адресов админки упало с 9 996 за 9-е до 11 за 10-е и 1 за 11-е, а IP, отправивший 22 391 запрос за три часа, со следующего дня не прислал ни одного.
Для сканера лимит включён совсем недавно, поэтому полных суточных данных ещё нет; судить предстоит по тому, насколько снизится на следующей неделе число страниц 404, сгенерированных WordPress (2 563 за 9-е, 2 612 за 10-е, 1 630 за 11-е). Ошибок 502·504 у посетителей не было ни до, ни после.
Эти цифры подсчитаны напрямую по логам сервера WordPress на Nginx, Apache и php-fpm, а выводы сделаны не по числу предупреждений, а по ошибкам посетителей и числу запросов, доходящих до PHP.
🔢 FAQ & рекомендуемые материалы
Кеш
Кеш сайта & внутренний кеш сервера
По уровням объясняет, как кешированные запросы обходятся без PHP.
Безопасность
Уязвимости безопасности wp-config.php
Как защитить файл настроек, на который нацелены сканеры, правами доступа и правилами сервера.
Linux
Команды Linux: 15 категорий
Таблица, где команды разделены на 15 категорий, включая grep и awk, которые использовались для подсчёта логов.
ℹ️ О партнёрских ссылках
Материалы этого сайта содержат партнёрские ссылки. Если посетитель покупает товар или услугу по такой ссылке, сайт получает комиссию от продавца. При этом сумма, которую платит покупатель (при акционных скидках сумма снижается ↓), не увеличивается. Указанные цены, скидки и наличие актуальны на момент написания и могут отличаться, поэтому перед покупкой уточняйте их у продавца. Товары выбираются и оцениваются по нашим собственным критериям, а комиссия не влияет ни на порядок, ни на содержание обзоров.