«Сайт тормозит» — постановка задачи, с которой начинается половина обращений на поддержку. PHP-FPM здесь удобная отправная точка: он находится между веб-сервером и кодом и видит, сколько запросов обрабатывается, сколько ждёт очереди и какие из них выполняются подозрительно долго.
Разберём диагностику по шагам — и главное, чего делать не надо, пока не найдена причина.
Введение
Когда веб-сайт на PHP начинает отвечать медленнее, нужно понять, что именно делают активные рабочие процессы. Статья разбирает систематический подход к диагностике нагрузки на PHP-FPM.
Основные понятия
| Термин | Описание |
|---|---|
| PHP-FPM pool | Группа рабочих процессов |
| active processes | Процессы, обрабатывающие запросы |
| idle processes | Свободные процессы в ожидании |
| listen queue | Очередь необработанных запросов |
Последовательность диагностики
- Проверить статус пула через php-fpm
- Определить URI и скрипты, занимающие воркеры
- Активировать slowlog для обнаружения зависаний
- Анализировать конкретный процесс через инструменты Linux
- Только затем корректировать параметры пула
Конфигурация 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
; Slowlog
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.logПолучение статуса пула
Через curl
# Краткий статус
curl http://127.0.0.1/fpm-status
# Полный статус с процессами
curl http://127.0.0.1/fpm-status?full
# JSON формат
curl http://127.0.0.1/fpm-status?json
# Полный статус в JSON
curl http://127.0.0.1/fpm-status?json&fullПример 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"
}
]
}Ключевые метрики
| Поле | Значение |
|---|---|
| active processes | Текущее количество занятых воркеров |
| idle processes | Свободные воркеры |
| listen queue | Запросы в очереди (> 0 = проблема) |
| max children reached | Был ли достигнут лимит |
| slow requests | Превысившие таймаут запросы |
| request duration | Длительность в микросекундах |
Если listen queue > 0 — воркеров не хватает, запросы ждут в очереди. Но увеличивать pm.max_children без понимания причины — плохая идея.
Получение статуса из PHP
<?php
if (!function_exists('fpm_get_status')) {
http_response_code(500);
echo json_encode([
'error' => '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 процессы
$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);Slowlog
Slowlog — самый полезный инструмент из всех перечисленных, и включать его стоит первым. Метрики пула отвечают на вопрос «плохо ли сейчас», а slowlog — на вопрос «что именно долго выполняется», причём со стеком вызовов на момент срабатывания порога.
Практически это выглядит так: ставите request_slowlog_timeout в 3–5 секунд, ждёте день и смотрите, какие функции повторяются в верхушках стеков. Обычно виноваты две-три конкретные вещи — запрос к внешнему API без таймаута, тяжёлый SQL или генерация чего-нибудь в цикле, — и находятся они быстрее, чем при любом другом подходе.
Накладные расходы у slowlog небольшие, поэтому его разумно держать включённым постоянно, а не только во время расследований.
Slowlog записывает стек вызовов для запросов, превысивших таймаут:
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
slowlog = /var/log/php-fpm/www-slow.logПример вывода
[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:32Slowlog показывает: какой метод завис и на какой строке. В примере выше проблема в curl_exec() — внешний API не отвечает.
Анализ на уровне Linux
Список процессов php-fpm
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd | grep '[p]hp-fpm'Статистика по ресурсам (pidstat)
pidstat -u -r -d -p 24731 1 5Показывает использование CPU, памяти и дисковые операции за 5 секунд.
Информация через /proc/
# Человекочитаемое состояние процесса
cat /proc/24731/status
# Машиночитаемый формат
cat /proc/24731/stat
# Командная строка процесса
tr '\0' ' ' < /proc/24731/cmdline
# Текущая директория
readlink /proc/24731/cwd
# Количество открытых дескрипторов
ls /proc/24731/fd | wc -lИнтерпретация метрик
Много активных процессов ≠ высокое использование CPU
Воркеры могут ждать:
- Ответ от базы данных
- Внешние API (curl_exec)
- Блокировки сессии
- Дисковые операции
- Сетевые операции
Ловушка fastcgi_finish_request()
echo json_encode(['status' => 'ok']);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// Процесс продолжает работать!
doVeryLongTask();Функция отправляет ответ клиенту, но процесс остаётся занят до завершения скрипта. Это быстро исчерпает пул воркеров.
Рекомендации:
- Закрывать сессию через
session_write_close() - Выносить долгие операции в очередь или фоновые процессы
Практический сценарий
Когда сайт отдаёт 504:
- Проверить
/fpm-status?json&full - Найти активные процессы и повторяющиеся URI
- Посмотреть slowlog для стека вызовов
- Запустить pidstat для проверки ресурсов
- Определить причину и применить решение
Справочные команды
# Краткий статус пула
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Действия после диагностики
Исправления относятся к четырём категориям (в порядке приоритета):
- Оптимизация кода — убрать тяжёлые циклы, добавить кэширование
- Оптимизация зависимостей — ускорить SQL, Redis, HTTP-вызовы
- Архитектурное разделение — перенести долгие операции в очередь
- Тюнинг FPM — корректно установить параметры пула
Про pm.max_children — параметр, который крутят первым и чаще всего зря.
Логика «процессов не хватает, добавим ещё» интуитивна и обычно ошибочна. Каждый процесс занимает память — на типичном приложении это десятки мегабайт. Подняв лимит до значения, при котором суммарное потребление превышает объём памяти сервера, вы получаете не производительность, а уход в подкачку и убийство процессов системой под нагрузкой — то есть отказ ровно в тот момент, когда нагрузка максимальна.
Верхняя граница считается просто: доступная память минус память под базу и служебные процессы, делённая на среднее потребление одного процесса (ps или pidstat покажут реальное значение). Полученное число — потолок, а не цель.
И главное: очередь запросов означает, что запросы обрабатываются слишком долго, а не что процессов мало. Добавление процессов лечит симптом и только до следующего роста трафика. Порядок работ ниже отражает именно это.
Применяйте исправления в указанном порядке. Соблазн начать с четвёртого пункта велик — это одна строка в конфиге против дня работы с кодом, — но pm.max_children без оптимизации только отодвигает проблему и часто делает отказ более резким.
И правило, экономящее больше всего времени: меняйте по одному параметру и измеряйте. Конфиг, изменённый в пяти местах разом, не позволяет понять, что помогло, а что навредило.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.