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

URL: https://dmeshcheryakov.ru/blog/php-fpm-diagnostics/
Раздел: DevOps
Теги: PHP, PHP-FPM, DevOps, Performance
Опубликовано: 2025-02-24
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

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

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

## Введение

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

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

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

## Как диагностировать PHP-FPM по порядку

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
```

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

```
[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` без оптимизации только отодвигает проблему и часто делает отказ более резким.

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