Что такое наблюдаемость в PHP-сервисе

Наблюдаемость — это способность понять, что происходит с приложением в production, не подключаясь к серверу вслепую. Для PHP это особенно важно из-за двух разных моделей выполнения. В классическом PHP-FPM процесс обычно обслуживает один запрос и освобождается. В RoadRunner, Swoole/OpenSwoole и queue workers код живёт дольше, а значит ошибки состояния, утечки памяти и «залипшие» соединения становятся обычной эксплуатационной темой. Это напрямую связано с PHP-FPM, RoadRunner и долгоживущие воркеры, Swoole и OpenSwoole и Очереди, фоновые задачи и воркеры.

Обычно наблюдаемость держится на трёх слоях: logs, metrics и traces. Логи отвечают на вопрос «что случилось?», метрики — «насколько часто и насколько плохо?», trace — «где именно запрос потерял время?». Health checks стоят рядом: они не объясняют проблему глубоко, но помогают load balancer, supervisor или Kubernetes понять, можно ли слать процессу трафик.

flowchart TD A[HTTP request / job / WebSocket event] --> B[Logs: что произошло] A --> C[Metrics: сколько, как часто, насколько медленно] A --> D[Traces: путь через приложение, БД, очередь и внешние API] B --> E[Поиск инцидента] C --> F[Алерты и SLO] D --> G[Поиск узкого места] H[Health checks] --> I[Load balancer / supervisor / orchestrator] I --> J[Трафик только на готовые процессы]
flowchart TD
    A[HTTP request / job / WebSocket event] --> B[Logs: что произошло]
    A --> C[Metrics: сколько, как часто, насколько медленно]
    A --> D[Traces: путь через приложение, БД, очередь и внешние API]
    B --> E[Поиск инцидента]
    C --> F[Алерты и SLO]
    D --> G[Поиск узкого места]
    H[Health checks] --> I[Load balancer / supervisor / orchestrator]
    I --> J[Трафик только на готовые процессы]
Наблюдаемость PHP-сервиса складывается из логов, метрик, traces и health checks; каждый слой отвечает на свой вопрос.
Quick recall
Почему для PHP-сервиса недостаточно просто «посмотреть ошибку на сервере», особенно в production?

Логи: не только stack trace

В production PHP должен логировать ошибки, а не показывать их пользователю. Это пересекается с Конфигурация безопасности PHP: display_errors=Off, log_errors=On, понятный error_log, а в веб-ответе — нейтральное сообщение без путей, SQL и секретов.

Минимальный php.ini для production обычно выглядит примерно так:

; production
error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log

Но просто «писать текст в файл» быстро становится тесно. Для сервисов удобнее структурированные JSON-логи: request id, route, user id или tenant id, длительность, HTTP status, exception class. Не надо складывать туда пароль, cookie, access token, полное тело платежного webhook или персональные данные без необходимости.

<?php

$requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));
$startedAt = hrtime(true);

try {
    // обработка запроса
} catch (Throwable $e) {
    error_log(json_encode([
        'level' => 'error',
        'request_id' => $requestId,
        'exception' => $e::class,
        'message' => $e->getMessage(),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
    ], JSON_THROW_ON_ERROR));

    http_response_code(500);
    echo 'Internal Server Error';
} finally {
    $durationMs = (hrtime(true) - $startedAt) / 1_000_000;
    error_log(json_encode([
        'level' => 'info',
        'request_id' => $requestId,
        'duration_ms' => round($durationMs, 2),
        'uri' => $_SERVER['REQUEST_URI'] ?? null,
    ], JSON_THROW_ON_ERROR));
}

В реальном проекте чаще используют PSR-3 logger через framework или библиотеку, но смысл тот же: лог должен быть пригоден для поиска, группировки и алертов.

Quick recall
Почему в production PHP обычно ставят `display_errors=Off`, но `log_errors=On`?

Метрики: что мерить в PHP-FPM

Для PHP-FPM первая полезная точка — status page. Она включается через pm.status_path в pool-конфиге и должна быть закрыта от внешнего интернета: страница раскрывает URL запросов и информацию о ресурсах. Её обычно отдают только localhost, внутреннему мониторингу или exporter’у.

; www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log

На status page смотрят не ради красоты, а ради решений: хватает ли воркеров, есть ли очередь ожидания, сколько active/idle processes, не растёт ли max children reached. Если listen queue растёт, пользователи ждут свободный FPM-worker. Если max children reached появляется регулярно, надо разбираться: мало процессов, медленные SQL-запросы, внешний API тормозит или код держит worker слишком долго.

pool:                 www
process manager:      dynamic
start time:           15/Jun/2026:09:12:44 +0000
accepted conn:        184203
listen queue:         17
max listen queue:     41
idle processes:       0
active processes:     24
total processes:      24
max active processes: 24
max children reached: 128
slow requests:        36

Slowlog особенно полезен, когда «всё иногда медленно». FPM может записать stack trace запроса, который превысил request_slowlog_timeout. Это не заменяет profiler, но быстро показывает класс проблем: тяжёлый шаблон, N+1 запрос, медленный HTTP-клиент, синхронная генерация PDF внутри веб-запроса.

[15-Jun-2026 09:18:21]  [pool www] pid 1842
script_filename = /srv/app/public/index.php
[0x00007f4a6c01a720] executeQuery() /srv/app/src/OrderRepository.php:88
[0x00007f4a6c01a680] loadItems() /srv/app/src/InvoiceController.php:42
[0x00007f4a6c01a5d0] render() /srv/app/public/index.php:19
Quick recall
Что означает рост `listen queue` на PHP-FPM status page?

Метрики для долгоживущих воркеров

У долгоживущих процессов другой набор риска. Для RoadRunner, Swoole/OpenSwoole и queue workers важны не только latency и error rate, но и память на воркер, число обработанных jobs, рестарты, возраст процесса, размер очереди, retry rate, dead-letter count. Если worker после каждой сотни jobs ест всё больше RAM, это не «особенность PHP», а сигнал искать удержанные ссылки, статические кэши, singleton state или ресурсы, которые не закрываются.

Типичный защитный приём — ограниченный lifetime: перезапускать worker после N запросов/jobs или при достижении memory limit. Это не лечит утечку, но ограничивает ущерб. Важно, чтобы restart был graceful: процесс перестаёт брать новую работу, завершает текущую и только потом умирает. Иначе появятся дубликаты jobs, оборванные WebSocket-сессии или частично выполненные операции. Здесь помогает идемпотентность из темы Очереди, фоновые задачи и воркеры.

Tracing: где потерялись миллисекунды

Trace связывает путь одного запроса через приложение, базу, кэш, очередь и внешние API. Для PHP это особенно полезно там, где один HTTP-запрос кладёт job, job идёт в worker, worker вызывает платежный API, а браузер получает событие через WebSocket, SSE и realtime в PHP.

Минимальная дисциплина без полноценного OpenTelemetry — прокидывать correlation id:

<?php

$traceId = $_SERVER['HTTP_TRACEPARENT']
    ?? $_SERVER['HTTP_X_REQUEST_ID']
    ?? bin2hex(random_bytes(16));

$job = [
    'type' => 'send_receipt',
    'order_id' => 4812,
    'trace_id' => $traceId,
];

$queue->push($job);

С OpenTelemetry можно собирать spans автоматически или вручную, но не стоит начинать с «покрыть всё». Практичнее сначала инструментировать входящие HTTP-запросы, SQL, HTTP-клиент, queue publish/consume и самые дорогие доменные операции.

Health checks: живой — не значит готов

Health endpoint бывает двух типов. Liveness отвечает: «процесс вообще жив?». Readiness отвечает: «ему можно отдавать трафик?». Для PHP-FPM это может быть /fpm-ping плюс отдельная прикладная /healthz. Для долгоживущего сервера readiness должен учитывать состояние event loop, подключение к БД, broker очереди, заполненность внутренних буферов и режим graceful shutdown.

{
  "status": "ready",
  "checks": {
    "database": "ok",
    "queue": "ok",
    "worker_accepting_jobs": true,
    "graceful_shutdown": false
  }
}

Плохой health check делает настоящий SQL SELECT *, зовёт внешний API или прогревает кэш. Хороший — дешёвый, быстрый, предсказуемый. Он не должен сам становиться источником нагрузки.

OPcache, деплой и странные «старые» ошибки

OPcache хранит скомпилированный bytecode PHP-скриптов в памяти. Это почти всегда нужно в production, но при деплое важно понимать настройки opcache.validate_timestamps, opcache.revalidate_freq, preload и reset/invalidate. Если timestamp validation выключен, новый код может не подхватиться без reload/reset. Если preload используется, часть классов загружается при старте процесса и живёт до перезапуска.

Для диагностики можно смотреть opcache_get_status(false) и opcache_get_configuration(), но такие endpoints нельзя оставлять публичными. Они раскрывают внутренние пути и настройки.

<?php

$status = opcache_get_status(false);

var_dump([
    'enabled' => $status['opcache_enabled'] ?? null,
    'restart_pending' => $status['restart_pending'] ?? null,
    'used_memory' => $status['memory_usage']['used_memory'] ?? null,
    'free_memory' => $status['memory_usage']['free_memory'] ?? null,
]);

Если после деплоя часть серверов видит новый код, а часть старый, проверяют не только Git SHA. Смотрят reload PHP-FPM, OPcache, preload, symlink-based deploy, shared volume, permissions и то, какой именно контейнер обслуживает запрос.

Практический порядок диагностики

Когда PHP-сервис «тормозит», не начинайте с переписывания на async. Сначала проверьте: растёт ли FPM listen queue, есть ли slowlog, какие routes дают p95/p99 latency, не упирается ли БД, не ждёт ли код внешний API, не заполнена ли очередь, не рестартуют ли воркеры по памяти. Если это realtime endpoint, отдельно смотрите количество открытых соединений, reconnect rate и proxy timeout.

Для классического PHP-FPM нормальная цель — короткие requests, понятные логи, контролируемый pool size, OPcache без сюрпризов и health checks, закрытые от внешнего мира. Для долгоживущего PHP — всё это плюс дисциплина состояния: очищать request-scoped данные, ограничивать lifetime воркеров, следить за памятью и уметь graceful restart.

См. также

Sources

  1. PHP Manual: FPM status page
  2. PHP Manual: FPM configuration
  3. PHP Manual: Error handling runtime configuration
  4. PHP Manual: OPcache
  5. PHP Manual: opcache_get_status
  6. OpenTelemetry PHP documentation