Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек

OPcache ускоряет PHP, но под нагрузкой может «захлебнуться»: заканчивается память, растёт фрагментация, падает hit rate. Разбираем, какие метрики смотреть, как собрать их в Prometheus и быстро ловить утечки.
Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек

OPcache — один из самых дешёвых и эффективных способов ускорить PHP: он кэширует байткод и экономит CPU на повторной компиляции. Но как и любой кэш, OPcache требует наблюдения. Без мониторинга легко получить «загадочные» падения производительности, внезапные рестарты кэша, рост времени отклика после деплоя или в прайм-тайм. В этой статье разберём, что именно мониторить, как без лишних зависимостей отдать метрики для Prometheus, какие алерты действительно полезны в проде и как быстро понять, есть ли утечка памяти или проблема с конфигурацией.

Зачем мониторить OPcache

Классические симптомы проблем с OPcache выглядят так: при трафиковых «волнах» резко растёт нагрузка на CPU и время отклика; после одной-двух недель аптайма кэш начинает «перезапускаться»; при деплое приложение нагревает CPU, а иногда и вываливается в 502. Большую часть таких кейсов можно предупредить, если заранее смотреть на состояние кэша и автоматически сигналить о деградации.

OPcache хранит два больших набора данных: пул памяти для закэшированных скриптов (байткода) и таблицу интернированных строк. У обоих есть общий объём, текущая занятость, «потери» (wasted/fragmented), а также счётчики попаданий/промахов, ограничений по хеш-таблице и перезапускам. Это и есть фундамент метрик.

Ключевые метрики и как их интерпретировать

Ниже — список метрик, которые стоит собрать и визуализировать. Названия даны в духе Prometheus, но фактически они получаются из opcache_get_status() и opcache_get_configuration().

  • opcache_memory_used_bytes / opcache_memory_free_bytes / opcache_memory_wasted_bytes — общий объём, свободное и «потерянное» (фрагментированное) место пула байткода. Важна динамика. Нормально, когда wasted держится < 5–10% и не растёт бесконечно.
  • opcache_interned_used_bytes / opcache_interned_free_bytes — использование пула интернированных строк. Если он стабильно упирается в лимит, увеличивайте opcache.interned_strings_buffer.
  • opcache_cached_scripts — число закэшированных файлов. Ограничивается opcache.max_accelerated_files; при близком к 100% заполнении растут коллизии и промахи.
  • opcache_hits_total / opcache_misses_total / opcache_hit_rate — попадания/промахи и кумулятивный hit rate. В проде целимся в > 98–99%.
  • opcache_hash_table_used_ratio — заполнение хеш-таблицы ключей. Если > 0.95 — увеличивайте opcache.max_accelerated_files.
  • opcache_restarts_total и детализация: oom_restarts, manual_restarts, hash_restarts. Любой рост в рабочее время — повод для алерта и разбора.
  • opcache_blacklist_miss_ratio — следствие блок-листа файлов. Обычно ~0; рост намекает на лишние записи или динамически генерируемые пути.
  • revalidate_freq и validate_timestamps — косвенно влияют на промахи и накладные расходы. На проде при частых деплоях подойдёт validate_timestamps=1 и revalidate_freq=2–10, при атомарных релизах и opcache_reset() — можно отключить проверки.

Сигналы утечек: монотонный рост wasted и/или интернированных строк без стабилизации; рост количества скриптов при неизменном релизе; частые OOM/hashing рестарты без всплесков трафика.

Визуализация метрик OPcache: память, фрагментация и hit rate

Быстрый экспорт метрик: минимальный endpoint на PHP

Самый простой путь — опубликовать локальный endpoint, который генерирует текст в формате Prometheus. Его можно скрейпить напрямую Prometheus-ом или собирать через textfile collector node_exporter. Скрипт выполняется под тем же SAPI, что и приложение (php-fpm), поэтому данные точно про «боевой» кэш.

PHP-скрипт для выдачи метрик

<?php
// /var/www/site/public/opcache_metrics.php
// Мини-экспортер OPcache для Prometheus

header('Content-Type: text/plain; version=0.0.4');
$status = opcache_get_status(false);
$config = opcache_get_configuration();

function println($line) { echo $line . "\n"; }

$mem = $status['memory_usage'];
$ints = $status['interned_strings_usage'];
$stat = $status['opcache_statistics'];

println("# HELP opcache_memory_used_bytes OPcache used memory");
println("# TYPE opcache_memory_used_bytes gauge");
println("opcache_memory_used_bytes " . (int)$mem['used_memory']);
println("# HELP opcache_memory_free_bytes OPcache free memory");
println("# TYPE opcache_memory_free_bytes gauge");
println("opcache_memory_free_bytes " . (int)$mem['free_memory']);
println("# HELP opcache_memory_wasted_bytes OPcache wasted memory");
println("# TYPE opcache_memory_wasted_bytes gauge");
println("opcache_memory_wasted_bytes " . (int)$mem['wasted_memory']);

println("# HELP opcache_interned_used_bytes Interned strings used memory");
println("# TYPE opcache_interned_used_bytes gauge");
println("opcache_interned_used_bytes " . (int)$ints['used_memory']);
println("# HELP opcache_interned_free_bytes Interned strings free memory");
println("# TYPE opcache_interned_free_bytes gauge");
println("opcache_interned_free_bytes " . (int)$ints['free_memory']);

println("# HELP opcache_cached_scripts Number of cached scripts");
println("# TYPE opcache_cached_scripts gauge");
println("opcache_cached_scripts " . (int)$stat['num_cached_scripts']);
println("# HELP opcache_max_cached_keys Max cached keys (from config)");
println("# TYPE opcache_max_cached_keys gauge");
println("opcache_max_cached_keys " . (int)$config['directives']['opcache.max_accelerated_files']);

println("# HELP opcache_hits_total OPcache hits");
println("# TYPE opcache_hits_total counter");
println("opcache_hits_total " . (int)$stat['hits']);
println("# HELP opcache_misses_total OPcache misses");
println("# TYPE opcache_misses_total counter");
println("opcache_misses_total " . (int)$stat['misses']);

$hitRate = $stat['opcache_hit_rate'] ?? 0;
println("# HELP opcache_hit_rate Current hit rate percent");
println("# TYPE opcache_hit_rate gauge");
println("opcache_hit_rate " . (float)$hitRate);

println("# HELP opcache_restarts_total Restarts by reason");
println("# TYPE opcache_restarts_total counter");
println("opcache_restarts_total{reason=\"oom\"} " . (int)$stat['oom_restarts']);
println("opcache_restarts_total{reason=\"hash\"} " . (int)$stat['hash_restarts']);
println("opcache_restarts_total{reason=\"manual\"} " . (int)$stat['manual_restarts']);

$hashUsed = $status['cache_full'] ? 1 : 0; // простейший индикатор
println("# HELP opcache_cache_full Cache full flag");
println("# TYPE opcache_cache_full gauge");
println("opcache_cache_full " . $hashUsed);

// Оценка заполнения хеш-таблицы (если доступно)
if (isset($stat['max_cached_keys']) && isset($stat['num_cached_keys'])) {
    $ratio = 0.0;
    if ($stat['max_cached_keys'] > 0) {
        $ratio = $stat['num_cached_keys'] / $stat['max_cached_keys'];
    }
    println("# HELP opcache_hash_table_used_ratio Hash table fill ratio");
    println("# TYPE opcache_hash_table_used_ratio gauge");
    println("opcache_hash_table_used_ratio " . $ratio);
}

Скрипт отдаёт набор метрик в формате Prometheus. Для продакшена обязательно ограничьте доступ только с localhost или с IP сборщика мониторинга, и не забудьте исключить из автодеплоя на публичные пути.

Пример защиты локации в Nginx

location /metrics/opcache {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/site/public/opcache_metrics.php;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

Теперь Prometheus может скрейпить локальный endpoint по пути /metrics/opcache. Если используете node_exporter с textfile collector, можно периодически забирать этот endpoint локальным curl и складывать в файл.

Сбор через textfile collector

# /usr/local/bin/opcache_metrics_pull.sh
#!/usr/bin/env bash
set -euo pipefail
curl -sS http://127.0.0.1/metrics/opcache > /var/lib/node_exporter/textfile_collector/opcache.prom
# systemd service
[Unit]
Description=Pull OPcache metrics into textfile collector

[Service]
Type=oneshot
ExecStart=/usr/local/bin/opcache_metrics_pull.sh

[Install]
WantedBy=multi-user.target
# systemd timer
[Unit]
Description=Pull OPcache metrics every 30s

[Timer] 
AccuracySec=1s

[Install]
WantedBy=timers.target

Такой подход минималистичен и даёт точные метрики именно из php-fpm, а не из отдельного CLI-процесса. Важно: не включайте opcache.enable_cli только ради мониторинга, иначе вы начнёте смотреть на «другой» OPcache.

Если вы выносите сбор метрик и визуализацию на отдельный узел, удобнее держать мониторинг на управляемом сервере с предсказуемыми ресурсами — например, на VDS.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Рекомендуемые алерты для Prometheus

Правила ниже рассчитаны на прод с устойчивым трафиком. Пороговые значения подберите под свой код и размер кэша. Лучше стартовать мягко (Warning) и постепенно ужесточать.

groups:
- name: php-opcache
  rules:
  - alert: OpcacheHitRateLow
    expr: opcache_hit_rate < 98
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "OPcache hit rate low (< 98%)"
      description: "Hit rate опустился ниже 98% в течение 10 минут. Проверьте max_accelerated_files, revalidate_freq и промахи после деплоя."

  - alert: OpcacheMemoryFreeLow
    expr: opcache_memory_free_bytes < 32e6
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "OPcache free memory low"
      description: "Свободной памяти в пуле байткода < 32 МБ. Рассмотрите увеличение opcache.memory_consumption или чистку кэша при деплое."

  - alert: OpcacheWastedHigh
    expr: opcache_memory_wasted_bytes / (opcache_memory_free_bytes + opcache_memory_used_bytes + opcache_memory_wasted_bytes) > 0.10
    for: 30m
    labels:
      severity: warning
    annotations:
      summary: "OPcache wasted memory > 10%"
      description: "Фрагментация/потери превышают 10% стабильно 30 минут. Возможна утечка или неудачная стратегия деплоев."

  - alert: OpcacheHashTableNearFull
    expr: opcache_hash_table_used_ratio > 0.95
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "OPcache hash table near full"
      description: "Хеш-таблица почти заполнена. Увеличьте opcache.max_accelerated_files."

  - alert: OpcacheRestartsSpike
    expr: increase(opcache_restarts_total[15m]) > 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "OPcache restarts detected"
      description: "За 15 минут зафиксированы рестарты OPcache (OOM/hash/manual). Нужна немедленная проверка."

  - alert: OpcacheInternedFreeLow
    expr: opcache_interned_free_bytes < 4e6
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "Interned strings free low"
      description: "Мало свободной памяти интернированных строк. Увеличьте opcache.interned_strings_buffer."

Быстрая диагностика утечек и аномалий

Когда алерты сработали, действуем по чек-листу. Цель — максимально быстро понять, это реальная утечка, фрагментация из-за паттерна деплоя или просто конфигурация слишком «узкая».

1) Проверяем конфигурацию OPcache

Базовый прод-набор для современных приложений (PHP 8.1+):

; php.ini / pool.ini
opcache.enable=1
opcache.enable_cli=0
opcache.jit=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=60000
opcache.validate_timestamps=1
opcache.revalidate_freq=5
opcache.save_comments=1
opcache.fast_shutdown=1

Если кода много, opcache.memory_consumption поднимайте до 384–512 МБ, а opcache.max_accelerated_files — до 100–200k. Не бойтесь «переборщить»: лишние десятки мегабайт обычно дешевле, чем повторные компиляции под нагрузкой. Дополнительно посмотрите рекомендации по лимитам PHP в материале о настройке php.ini и .user.ini.

2) Смотрим динамику wasted и interned

Если обе метрики растут монотонно днями и не стабилизируются — вероятна логическая «утечка»: приложение генерирует новые уникальные пути/скрипты (eval, динамические include), или часто переименовывает файлы при деплое. Проверьте, нет ли исполняемого кода, генерируемого с уникальными именами, и что деплой не плодит новые пути в хеш-таблице (например, релизы с уникальными директориями без опции realpath cache и без opcache_reset()).

3) Ищем горячие каталоги/файлы

Снимем срез, какие каталоги занимают больше всего памяти в кэше:

<?php
// ad-hoc: top по памяти скриптов в OPcache
$s = opcache_get_status(true);
$byDir = [];
foreach (($s['scripts'] ?? []) as $path => $meta) {
    $dir = dirname($path);
    $mem = $meta['memory_consumption'] ?? 0;
    $byDir[$dir] = ($byDir[$dir] ?? 0) + $mem;
}
arsort($byDir);
$limit = 20;
foreach (array_slice($byDir, 0, $limit, true) as $dir => $bytes) {
    echo $dir . "\t" . $bytes . "\n";
}

Если в топе «всплывают» каталоги storage/cache, временные каталоги или директории релизов с уникальными именами — вероятно, кэш забивается артефактами деплоя или runtime-генерации.

4) Проверяем стратегию деплоя

  • Атомарные релизы через симлинк: после переключения симлинка вызывайте opcache_reset() единоразово. Иначе кэш будет хранить старые пути и байткод до перезапуска FPM.
  • Если включены проверки таймштампов (validate_timestamps=1), не ставьте revalidate_freq=0 на нагруженных проектах — это может дать лишнюю нагрузку от постоянных stat.
  • Не кэшируйте динамически генерируемые PHP-файлы. Добавьте их в blacklist.

5) Быстрая стабилизация под нагрузкой

  • Временно увеличьте opcache.memory_consumption и opcache.interned_strings_buffer (перезапуск FPM обязателен) — это даст «воздух» на разбор.
  • Если hash table почти полон — увеличьте opcache.max_accelerated_files, иначе растут промахи и коллизии.
  • При длительном росте wasted выполните контролируемый opcache_reset() в окно низкой нагрузки. Это снимет фрагментацию, но не решит первопричину.

Связь с php-fpm и размером пула

OPcache шарится всеми воркерами в рамках одного процесса менеджера, но на сервере может быть несколько пулов php-fpm, каждый с собственным OPcache. Убедитесь, что мониторите каждый пул, если на хосте несколько сайтов или разные версии PHP. На больших пулах следите за временем «прогрева» после деплоя: если все воркеры одновременно компилируют холодный код, CPU взлетит. Решения:

  • Прогрев в CI/CD: однократный обход критичных URL-ов после релиза для заполнения кэша.
  • Небольшая пауза между рестартом пула и включением трафика (drain + warmup).
  • Стабильное значение pm.max_children и предсказуемые лимиты памяти, чтобы не ловить массовые респавны.

Пример защиты endpoint метрик в Nginx

Чек-лист настройки с нуля

  1. Выберите целевые лимиты: начните с opcache.memory_consumption=256, opcache.interned_strings_buffer=16, opcache.max_accelerated_files=60000.
  2. Включите endpoint метрик под php-fpm и ограничьте доступ по IP.
  3. Соберите в Prometheus или через textfile collector. Добавьте графики: hit rate, free/wasted, interned usage, restarts.
  4. Включите алерты из примеров и оттестируйте на стенде, искусственно снижая лимиты.
  5. Зафиксируйте стратегию деплоя: когда и где вызывается opcache_reset(), как выполняется прогрев.
  6. Через неделю под реальной нагрузкой пересмотрите лимиты. Если алертов нет, можно аккуратно уменьшить — но помните о запасе.

FAQ и типовые ловушки

  • Hit rate 100%, но сайт тормозит. Проверьте не OPcache, а I/O, базу, сеть. А также JIT и расширения.
  • После деплоя резко растут промахи. Для atomic-деплоя вызывайте opcache_reset() либо дождитесь естественной инвалидации при validate_timestamps=1 и ненулевом revalidate_freq.
  • Постоянно растёт интернированная память. Ищите генерацию большого числа уникальных строк (шаблонизаторы, сериализация ключей, eval). Увеличьте interned_strings_buffer и устраните первопричину.
  • Рестарты без трафика. Проверьте крон-задачи и CLI-скрипты с opcache.enable_cli=1 — они не должны влиять на fpm, но иногда путают диагностику.
  • Несколько версий PHP на одном хосте. У каждой версии свой OPcache. Настраивайте и мониторьте отдельно.

Итоги

Мониторинг OPcache сводится к нескольким простым метрикам и паре здравых алертов. Главное — снимать именно метрики из fpm-пула, защищать endpoint и наблюдать динамику: hit rate, свободная память, wasted, интернированные строки, близость к лимитам и рестарты. Это позволяет заранее поймать деградацию, спокойно пройти деплой под нагрузкой и избежать ночных аварий, когда «вдруг пропал кеш».

С правильно подобранными лимитами и базовым прогревом OPcache работает стабильно месяцами. А мониторинг даст уверенность, что когда нагрузка вырастет в 2–3 раза, сервер выдержит без сюрпризов.

Поделиться статьей

Вам будет интересно

Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием OpenAI Статья написана AI Fastfox

Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием

Практическое руководство по Nginx SSI и subrequest: сборка страницы из блоков, фрагментное кэширование, разделение гостей и автори ...
rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти OpenAI Статья написана AI Fastfox

rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти

Большие файлы и S3 требуют точной настройки rclone: multipart‑загрузка, параллелизм потоков, контроль памяти и полосы, устойчивост ...
Maintenance mode правильно: 503, Retry‑After и кастомные страницы OpenAI Статья написана AI Fastfox

Maintenance mode правильно: 503, Retry‑After и кастомные страницы

Плановые работы не должны ломать индексацию и кэш, а пользователи — видеть понятную страницу. Разбираем правильный maintenance: ко ...