Как анализировать активные PHP-процессы
Когда сайт на PHP начинает отвечать медленно, снаружи это выглядит одинаково: растёт время ответа, появляются 502/504, сервер начинает есть больше CPU и памяти. Но реальный вопрос почти всегда один: что сейчас делают активные PHP-процессы и какой запрос занял воркеры php-fpm.
Эту тему удобно разбирать по слоям: от встроенной диагностики php-fpm до анализа конкретного PID в Linux. Ниже — практический разбор с примерами и нормальной последовательностью действий.
Что вообще считать активным PHP-процессом
В большинстве современных проектов под активными PHP-процессами имеют в виду воркеры php-fpm, которые прямо сейчас обрабатывают HTTP-запросы.
Важно не путать несколько сущностей:
- PHP-FPM pool — пул воркеров, например www.
- active processes — число воркеров, которые прямо сейчас заняты запросами.
- idle processes — свободные воркеры, которые ждут следующий запрос.
- listen queue — очередь запросов, которым не хватило свободного воркера.
Если активных процессов много, это ещё не обязательно проблема. Настоящая проблема начинается тогда, когда одновременно растут очередь, время выполнения и число ошибок на фронте.
С чего начинать диагностику
Правильная последовательность обычно такая:
- Посмотреть состояние пула через php-fpm status.
- Понять, какие URI и скрипты заняли воркеры.
- Включить slowlog, если запросы зависают внутри PHP-кода.
- Проверить конкретный PID через /proc/, ps и pidstat.
- Только после этого трогать pm.max_children, таймауты и лимиты.
Главная мысль простая: сначала находим узкое место, потом увеличиваем ресурсы. Иначе можно просто размазать проблему по большему числу процессов.
1. Самый полезный инструмент: страница статуса php-fpm
php-fpm умеет отдавать встроенную страницу статуса. Это лучший первый источник правды, потому что он показывает не абстрактную загрузку сервера, а состояние самого пула: сколько воркеров занято, есть ли очередь, какой запрос выполнялся последним и сколько он занял времени.
Минимальная конфигурация пула
; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.status_path = /fpm-status
ping.path = /fpm-ping
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
После изменения конфигурации нужно перезагрузить FPM. Саму страницу статуса нельзя открывать публично: она показывает URI запросов, путь к скриптам и состояние пула. Обычно её ограничивают локальным доступом или отдельным служебным location.
Если на пике нагрузки основной пул может быть полностью занят, полезно посмотреть на директиву pm.status_listen. Она создаёт отдельную точку обслуживания статуса, чтобы метрики были доступны даже тогда, когда рабочие воркеры уже закончились.
Запрос статуса
bash
curl http://127.0.0.1/fpm-status
curl http://127.0.0.1/fpm-status?full
curl http://127.0.0.1/fpm-status?json
curl http://127.0.0.1/fpm-status?json&full
Для автоматизации удобнее всего JSON.
Что означают ключевые поля
- active processes — сколько воркеров занято прямо сейчас.
- idle processes — сколько воркеров свободно.
- listen queue — сколько запросов ждут свободный воркер.
- max children reached — был ли достигнут лимит pm.max_children.
- slow requests — сколько запросов превысили request_slowlog_timeout.
- request duration — длительность последнего запроса в микросекундах.
- request uri и script — какой URI и какой PHP-скрипт заняли воркер.
- last request cpu и last request memory — сколько CPU и памяти потребил последний завершённый запрос.
Если listen queue > 0, значит запросы уже стоят в очереди. Если max children reached > 0, значит пул реально упирался в лимит. Это два самых важных сигнала.
Пример разбора JSON-ответа
{
"pool": "www",
"process manager": "dynamic",
"accepted conn": 18421,
"listen queue": 3,
"idle processes": 0,
"active processes": 20,
"total processes": 20,
"max active processes": 20,
"max children reached": 4,
"slow requests": 17,
"processes": [
{
"pid": 24731,
"state": "Running",
"request duration": 18852341,
"request method": "GET",
"request uri": "/catalog/?section=servers",
"script": "/var/www/project/public/index.php"
}
]
}
Из этого примера видно:
- Все 20 воркеров заняты.
- Свободных процессов нет.
- Новые запросы уже ждут в очереди.
- Лимит pm.max_children уже достигался.
- Один из запросов выполняется почти 19 секунд.
На практике этого уже достаточно, чтобы переходить от абстрактного "сайт тормозит" к конкретному URI или сценарию нагрузки.
2. Быстрый снимок прямо из PHP через fpm_get_status()
Если нужно получить состояние пула не через отдельный endpoint, а из PHP-кода, можно использовать fpm_get_status(). Функция доступна только тогда, когда скрипт действительно исполняется через FPM.
'fpm_get_status() недоступна вне PHP-FPM',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$status = fpm_get_status();
if ($status === false) {
http_response_code(500);
echo json_encode([
'error' => 'Не удалось получить статус пула',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$running = array_values(array_filter(
$status['procs'] ?? [],
static fn(array $proc): bool => ($proc['state'] ?? '') === 'Running'
));
usort($running, static function (array $a, array $b): int {
return ($b['request duration'] ?? 0) <=> ($a['request duration'] ?? 0);
});
echo json_encode([
'pool' => $status['pool'] ?? null,
'active_processes' => $status['active processes'] ?? null,
'idle_processes' => $status['idle processes'] ?? null,
'listen_queue' => $status['listen queue'] ?? null,
'running_processes' => $running,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
Такой скрипт удобен для внутренней админки или разовой диагностики. Но это не замена штатной страницы статуса: если пул уже перегружен, диагностический скрипт будет конкурировать с обычными запросами за те же воркеры.
3. Как понять, какой именно процесс завис
После того как вы увидели проблемный PID в статусе FPM, можно спуститься на уровень Linux.
Посмотреть список процессов php-fpm
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd | grep '[p]hp-fpm'
Здесь обычно интересны PID, процент CPU, процент памяти и время жизни процесса. Если один воркер существует давно и стабильно растёт по памяти, это повод проверить логику приложения и значение pm.max_requests.
Снять статистику по CPU, памяти и I/O
pidstat -u -r -d -p 24731 1 5
Команда покажет, тратит ли процесс CPU, ждёт ли диск и как ведёт себя память. Это важный момент: не каждый долгий PHP-процесс грузит процессор. Иногда он просто ждёт базу, Redis, внешний API, файловую систему или блокировку сессии.
Посмотреть информацию через /proc/
cat /proc/24731/status
cat /proc/24731/stat
tr '\0' ' ' < /proc/24731/cmdline
readlink /proc/24731/cwd
ls /proc/24731/fd | wc -l
Практически полезны такие файлы:
- /proc//status — человекочитаемое состояние, RSS, контекстные переключения, лимиты.
- /proc//stat — машинный формат, который используют ps и другие утилиты.
- /proc//cmdline — командная строка процесса.
- /proc//fd — открытые файловые дескрипторы.
Например, если в /proc//status вы видите большой VmRSS, процесс реально держит память. Если дескрипторов подозрительно много, можно проверить утечки соединений или файловых ресурсов.
4. slowlog: когда нужно видеть стек вызовов
Страница статуса показывает, какой процесс занят и каким URI. Но она не отвечает на главный вопрос разработчика: на какой строке кода процесс завис. Для этого нужен slowlog.
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
slowlog = /var/log/php-fpm/www-slow.log
Когда запрос выполняется дольше пяти секунд, FPM пишет в slowlog стек PHP-вызовов. Это часто самый быстрый способ увидеть, что тормозит:
-
- долгий SQL-запрос; - зависший HTTP-клиент; - медленная работа с файлами; - тяжёлый цикл в PHP; - блокировка сессии или файловая блокировка.
Фрагмент лога может выглядеть так:
[24-May-2026 14:17:08] [pool www] pid 24731
script_filename = /var/www/project/public/index.php
[0x00007f4eebf5f4c0] curl_exec() /var/www/project/src/Service/ExternalApi.php:84
[0x00007f4eebf5f3d0] request() /var/www/project/src/Controller/CatalogController.php:118
[0x00007f4eebf5f250] show() /var/www/project/public/index.php:32
Это уже не просто "активный PHP-процесс", а конкретный ответ: воркер завис на внешнем HTTP-запросе внутри ExternalApi.php:84.
5. Почему много активных PHP-процессов не всегда означает высокий CPU
Это одна из самых частых ошибок в интерпретации.
Воркеры могут быть активны, но при этом:
- ждать ответ от MySQL;
- ждать внешний API;
- стоять на блокировке сессии;
- ждать сеть или диск;
- висеть после fastcgi_finish_request().
Поэтому картина "20 активных процессов" сама по себе ничего не доказывает. Нужно смотреть комбинацию метрик:
- много Running и высокий CPU — вероятен тяжёлый PHP-код;
- много Running, но CPU низкий — вероятны ожидания I/O или блокировки;
- listen queue > 0 и idle processes = 0 — пул не успевает обслуживать входящий поток;
- slow requests растёт — запросы стабильно выходят за разумный порог времени;
- один и тот же request uri повторяется в нескольких воркерах — проблема локализована в конкретной странице или сценарии.
6. Отдельная ловушка: fastcgi_finish_request() не освобождает воркер
fastcgi_finish_request() полезна, когда нужно быстро отдать ответ клиенту и продолжить фоновые действия внутри того же запроса. Но у неё есть критическое ограничение: FPM-процесс остаётся занят до полного завершения скрипта.
Это значит, что такая конструкция:
'ok']);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
doVeryLongTask();
не делает код "бесплатным" для пула. Пользователь ответ уже получил, но воркер всё ещё занят долгой задачей. Если так сделать в массовом сценарии, можно быстро упереться в pm.max_children и получить очередь или 502.
Практические рекомендации здесь две:
- закрывайте сессию как можно раньше через session_write_close(), иначе следующие запросы пользователя могут блокироваться на сессии;
- длинные фоновые операции лучше выносить в очередь, cron или отдельный worker, а не держать FPM-процесс занятым.
7. Практический сценарий разбора проблемы
Представим, что сайт периодически отдаёт 504.
- Открываем /fpm-status?json&full и видим: active processes = 20, idle processes = 0, listen queue = 7.
- Смотрим массив процессов и замечаем, что половина воркеров занята URI /catalog/?section=servers.
- Проверяем slowlog и видим стек, который упирается в получение цен из внешнего API.
- Запускаем pidstat для одного из PID и убеждаемся, что CPU невысокий, зато запросы длятся долго.
- Вывод: проблема не в числе воркеров, а в синхронном внешнем запросе внутри страницы каталога.
В такой ситуации простое увеличение pm.max_children может слегка отсрочить аварию, но не уберёт первопричину. Правильнее кэшировать ответ, вынести интеграцию из пользовательского запроса или ограничить таймаут внешнего вызова.
8. Команды, которые стоит держать под рукой
# Краткий статус пула
curl http://127.0.0.1/fpm-status?json | jq
# Полный статус с процессами
curl http://127.0.0.1/fpm-status?json&full | jq
# Все процессы php-fpm
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd | grep '[p]hp-fpm'
# Наблюдение за конкретным PID
pidstat -u -r -d -p 24731 1 5
# Человекочитаемое состояние процесса
cat /proc/24731/status
# Командная строка процесса
tr '\0' ' ' < /proc/24731/cmdline
# Количество открытых дескрипторов
ls /proc/24731/fd | wc -l
9. Что делать после диагностики
Когда причина уже понятна, исправления обычно попадают в один из четырёх классов:
- оптимизация кода — убрать тяжёлый цикл, сократить сериализацию, добавить кэш;
- оптимизация зависимостей — ускорить SQL, Redis, файловые операции, HTTP-вызовы во внешние сервисы;
- архитектурное разделение — вынести долгие действия в очередь и фоновые воркеры;
- тюнинг FPM — корректно выставить pm.max_children, pm.max_requests, таймауты и slowlog.
Именно в таком порядке. Тюнинг пула без понимания причины помогает только временно.
Итог
Анализ активных PHP-процессов — это не одна команда и не один график. Надёжная схема выглядит так:
- php-fpm status показывает общую картину по пулу.
- fpm_get_status() помогает встроить диагностику в приложение.
- slowlog показывает, на каком участке PHP-кода завис запрос.
- ps, pidstat и /proc/ позволяют добраться до конкретного процесса на уровне ОС.
Если использовать эти инструменты вместе, можно быстро ответить на три главных вопроса:
- сколько воркеров занято;
- чем именно они заняты;
- почему они не освобождаются вовремя.