Главная/Статьи/Диагностика PHP-FPM: анализ активных процессов и slowlog

Диагностика PHP-FPM: анализ активных процессов и slowlog

Систематический подход к диагностике нагрузки на PHP-FPM: статус пула, slowlog, /proc и pidstat. Находим причину медленных ответов.

ДМ
Дмитрий Мещеряков
📅 24 февраля 2025 г.📖 6 мин чтения

«Сайт тормозит» — постановка задачи, с которой начинается половина обращений на поддержку. PHP-FPM здесь удобная отправная точка: он находится между веб-сервером и кодом и видит, сколько запросов обрабатывается, сколько ждёт очереди и какие из них выполняются подозрительно долго.

Разберём диагностику по шагам — и главное, чего делать не надо, пока не найдена причина.

Введение

Когда веб-сайт на PHP начинает отвечать медленнее, нужно понять, что именно делают активные рабочие процессы. Статья разбирает систематический подход к диагностике нагрузки на PHP-FPM.

Основные понятия

ТерминОписание
PHP-FPM poolГруппа рабочих процессов
active processesПроцессы, обрабатывающие запросы
idle processesСвободные процессы в ожидании
listen queueОчередь необработанных запросов

Последовательность диагностики

  1. Проверить статус пула через php-fpm
  2. Определить URI и скрипты, занимающие воркеры
  3. Активировать slowlog для обнаружения зависаний
  4. Анализировать конкретный процесс через инструменты Linux
  5. Только затем корректировать параметры пула

Конфигурация PHP-FPM

ini
; /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

bash
# Краткий статус
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-ответа

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
<?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 записывает стек вызовов для запросов, превысивших таймаут:

ini
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
slowlog = /var/log/php-fpm/www-slow.log

Пример вывода

text
[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
💡 Совет

Slowlog показывает: какой метод завис и на какой строке. В примере выше проблема в curl_exec() — внешний API не отвечает.

Анализ на уровне Linux

Список процессов php-fpm

bash
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd | grep '[p]hp-fpm'

Статистика по ресурсам (pidstat)

bash
pidstat -u -r -d -p 24731 1 5

Показывает использование CPU, памяти и дисковые операции за 5 секунд.

Информация через /proc/

bash
# Человекочитаемое состояние процесса
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()

php
echo json_encode(['status' => 'ok']);

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
}

// Процесс продолжает работать!
doVeryLongTask();

Функция отправляет ответ клиенту, но процесс остаётся занят до завершения скрипта. Это быстро исчерпает пул воркеров.

Рекомендации:

  • Закрывать сессию через session_write_close()
  • Выносить долгие операции в очередь или фоновые процессы

Практический сценарий

Когда сайт отдаёт 504:

  1. Проверить /fpm-status?json&full
  2. Найти активные процессы и повторяющиеся URI
  3. Посмотреть slowlog для стека вызовов
  4. Запустить pidstat для проверки ресурсов
  5. Определить причину и применить решение

Справочные команды

bash
# Краткий статус пула
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

Действия после диагностики

Исправления относятся к четырём категориям (в порядке приоритета):

  1. Оптимизация кода — убрать тяжёлые циклы, добавить кэширование
  2. Оптимизация зависимостей — ускорить SQL, Redis, HTTP-вызовы
  3. Архитектурное разделение — перенести долгие операции в очередь
  4. Тюнинг FPM — корректно установить параметры пула
⚠️ Важно

Про pm.max_children — параметр, который крутят первым и чаще всего зря.

Логика «процессов не хватает, добавим ещё» интуитивна и обычно ошибочна. Каждый процесс занимает память — на типичном приложении это десятки мегабайт. Подняв лимит до значения, при котором суммарное потребление превышает объём памяти сервера, вы получаете не производительность, а уход в подкачку и убийство процессов системой под нагрузкой — то есть отказ ровно в тот момент, когда нагрузка максимальна.

Верхняя граница считается просто: доступная память минус память под базу и служебные процессы, делённая на среднее потребление одного процесса (ps или pidstat покажут реальное значение). Полученное число — потолок, а не цель.

И главное: очередь запросов означает, что запросы обрабатываются слишком долго, а не что процессов мало. Добавление процессов лечит симптом и только до следующего роста трафика. Порядок работ ниже отражает именно это.

💡 Совет

Применяйте исправления в указанном порядке. Соблазн начать с четвёртого пункта велик — это одна строка в конфиге против дня работы с кодом, — но pm.max_children без оптимизации только отодвигает проблему и часто делает отказ более резким.

И правило, экономящее больше всего времени: меняйте по одному параметру и измеряйте. Конфиг, изменённый в пяти местах разом, не позволяет понять, что помогло, а что навредило.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.