OPcache для PHP: Настройка, Инвалидация и Мониторинг Hit Rate

Каждый раз, когда PHP-FPM обрабатывает запрос к WordPress, он открывает и парсит около 5000 файлов (ядро, плагины, тема). Без OPcache этот процесс повторяется при каждом HTTP-запросе, создавая огромную нагрузку на CPU и диск. OPcache решает эту проблему радикально: он кэширует скомпилированный байт-код в оперативной памяти, и PHP берёт готовые инструкции оттуда, минуя этап парсинга и компиляции.

На практике включение и правильная настройка OPcache даёт прирост производительности WordPress в 2-3 раза. Это самая простая и эффективная оптимизация серверной части, которая не требует изменений в коде приложения. Разбираем все аспекты: от базовой активации до продвинутого мониторинга и автоматической инвалидации при деплоях.

Что такое OPcache и как он работает

OPcache (Optimizer Cache) — встроенный в PHP модуль, начиная с версии 5.5. Его работа делится на три этапа:

  1. Первый запрос к файлу: PHP читает исходный .php-файл с диска, парсит его в абстрактное синтаксическое дерево (AST), компилирует в байт-код (opcode) и сохраняет в общую память OPcache
  2. Последующие запросы: PHP берёт готовый байт-код прямо из памяти, пропуская чтение диска и компиляцию. Это экономит 80-90% времени обработки
  3. Инвалидация: при изменении файла OPcache обнаруживает это (по timestamp или через принудительный сброс) и перекомпилирует только этот файл

Что именно кэшируется:

  • Все файлы ядра WordPress (wp-includes/, wp-admin/)
  • PHP-файлы активных плагинов и темы
  • Библиотеки из vendor/ (Composer-зависимости)
  • Кастомные PHP-файлы в functions.php и дочерних темах

Что НЕ кэшируется:

  • HTML-вывод (для этого нужен FastCGI Cache или плагин кэширования страниц)
  • Результаты SQL-запросов (это задача Redis Object Cache)
  • Статические файлы (CSS, JS, изображения — кэшируются Nginx)

Шаг 1: Проверка статуса OPcache

Прежде чем настраивать, убеждаемся, что OPcache активен. Через CLI:

php -v | grep -i opcache
# Ожидаемый вывод: with Zend OPcache v8.2.x

# Подробная информация о конфигурации
php -i | grep opcache

Для проверки через веб-интерфейс создаём временный файл opcache-status.php в корне сайта:

<?php
// Временный файл для проверки OPcache
if ( ! function_exists( 'opcache_get_status' ) ) {
    die( 'OPcache не установлен или отключен' );
}

$status = opcache_get_status( false );
$config = opcache_get_configuration();

echo '<h1>OPcache Status</h1>';
echo '<h2>Общая информация</h2>';
echo '<pre>';
print_r( [
    'enabled' => $status['opcache_enabled'],
    'memory_usage' => round( $status['memory_usage']['used_memory'] / 1024 / 1024, 2 ) . ' MB из ' . 
                      round( $status['memory_usage']['free_memory'] / 1024 / 1024, 2 ) . ' MB свободно',
    'hit_rate' => round( $status['opcache_statistics']['opcache_hit_rate'], 2 ) . '%',
    'cached_scripts' => $status['opcache_statistics']['num_cached_scripts'],
    'misses' => $status['opcache_statistics']['misses'],
    'hits' => $status['opcache_statistics']['hits'],
] );
echo '</pre>';

⚠️ Важно: обязательно удалите этот файл после проверки. Он раскрывает информацию о конфигурации сервера и должен быть недоступен публично.

Шаг 2: Оптимальные параметры для WordPress

Стандартные настройки OPcache в php.ini рассчитаны на небольшие скрипты. Для WordPress с его тысячами файлов их нужно существенно увеличить. Открываем /etc/php/8.2/fpm/conf.d/10-opcache.ini (путь зависит от версии PHP и ОС):

; === Включение OPcache ===
opcache.enable = 1
opcache.enable_cli = 1  ; Включаем и для CLI (WP-CLI, cron)

; === Память ===
; WordPress + плагины требуют 256-512 МБ для байт-кода
opcache.memory_consumption = 256

; Строковый кэш (для интернированных строк)
opcache.interned_strings_buffer = 32

; === Лимит файлов ===
; WordPress с 30+ плагинами использует 8000-12000 PHP-файлов
; Значение должно быть степенью двойки + 1 (для хеш-таблиц)
opcache.max_accelerated_files = 16229

; === Стратегия инвалидации ===
; 0 = проверять файлы при каждом запросе (медленно, но всегда актуально)
; 60 = проверять раз в минуту (оптимально для продакшна)
; 300 = проверять раз в 5 минут (если деплой редкий)
opcache.revalidate_freq = 60

; === Оптимизации ===
; Сохранять комментарии в байт-коде (нужно для PHPDoc)
opcache.save_comments = 1

; Быстрое завершение (экономит немного CPU)
opcache.fast_shutdown = 1

; Проверка существования файла (предотвращает ошибки при удалении)
opcache.validate_timestamps = 1

; === Предзагрузка (PHP 7.4+) ===
; Предзагружает часто используемые файлы в память при старте FPM
; Требует создания preload-скрипта (см. Шаг 6)
; opcache.preload = /var/www/wordpress/opcache-preload.php
; opcache.preload_user = www-data

Расчёт оптимального memory_consumption:

Один PHP-файл в среднем занимает 20-30 КБ в OPcache. Формула:

# Подсчёт количества PHP-файлов в WordPress
find /var/www/wordpress -name "*.php" | wc -l
# Пример вывода: 11245

# Расчёт памяти: 11245 файлов × 25 КБ = ~275 МБ
# Округляем вверх до 300-512 МБ для запаса

Если память OPcache закончится, новые файлы не будут кэшироваться, и hit rate упадёт. После изменения параметров обязательно перезагружаем PHP-FPM:

sudo systemctl restart php8.2-fpm

Шаг 3: Мониторинг hit rate

Hit rate — ключевая метрика эффективности OPcache. Показывает, какой процент запросов к файлам обслуживается из кэша, а не через компиляцию.

Целевые показатели:

  • > 99%: идеальная настройка, кэш работает эффективно
  • 95-99%: хорошо, но можно улучшить (увеличить memory_consumption или max_accelerated_files)
  • < 95%: проблема, кэш переполнен или неправильно настроен
  • < 50%: OPcache практически не работает

Скрипт для постоянного мониторинга:

#!/bin/bash
# /usr/local/bin/opcache-monitor.sh

# Получаем статус через WP-CLI (без создания публичных файлов)
STATUS=$(cd /var/www/wordpress && wp eval 'echo json_encode(opcache_get_status(false));' 2>/dev/null)

if [ -z "$STATUS" ]; then
    echo "OPcache недоступен"
    exit 1
fi

HIT_RATE=$(echo "$STATUS" | jq -r '.opcache_statistics.opcache_hit_rate' | cut -d. -f1)
USED_MEM=$(echo "$STATUS" | jq -r '.memory_usage.used_memory')
FREE_MEM=$(echo "$STATUS" | jq -r '.memory_usage.free_memory')
CACHED=$(echo "$STATUS" | jq -r '.opcache_statistics.num_cached_scripts')
MAX_FILES=$(echo "$STATUS" | jq -r '.opcache_statistics.max_cached_keys')

USED_MB=$((USED_MEM / 1024 / 1024))
FREE_MB=$((FREE_MEM / 1024 / 1024))

echo "=== OPcache Status ==="
echo "Hit Rate: ${HIT_RATE}%"
echo "Memory: ${USED_MB}MB used, ${FREE_MB}MB free"
echo "Scripts: ${CACHED} / ${MAX_FILES}"

# Алерт при низком hit rate
if [ "$HIT_RATE" -lt 95 ]; then
    echo "⚠️  WARNING: Hit rate below 95%"
    # Отправка в Telegram (настройка по аналогии со скриптом уведомлений о бэкапах)
    curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
        -d chat_id="$CHAT_ID" \
        -d text="⚠️ OPcache Alert: hit_rate=${HIT_RATE}%, used=${USED_MB}MB"
fi

# Алерт при нехватке памяти
if [ "$FREE_MB" -lt 20 ]; then
    echo "⚠️  WARNING: Less than 20MB free memory"
fi

Добавляем в cron для проверки каждые 15 минут:

*/15 * * * * /usr/local/bin/opcache-monitor.sh >> /var/log/opcache-monitor.log 2>&1

Шаг 4: Инвалидация кэша при деплое

Если opcache.revalidate_freq установлен в 60 секунд, изменения в PHP-файлах после деплоя появятся только через минуту. Для продакшна это критично — нужно сбрасывать OPcache сразу после выгрузки нового кода.

Метод 1: Через WP-CLI (рекомендуемый)

WP-CLI имеет встроенную команду для сброса OPcache. Добавляем её в конец CI/CD скрипта деплоя:

# После rsync или git pull
cd /var/www/wordpress
wp cache flush
wp eval 'if (function_exists("opcache_reset")) { opcache_reset(); echo "OPcache cleared\n"; }'

Метод 2: HTTP-эндпоинт для сброса

Для удалённых деплоев через GitHub Actions создаём защищённый endpoint:

// В functions.php темы или в кастомном плагине
add_action( 'init', function() {
    if ( ! isset( $_GET['opcache_reset'] ) ) {
        return;
    }
    
    // Проверка секретного токена
    if ( $_GET['opcache_reset'] !== CUSTOMCODE_OPCACHE_SECRET ) {
        wp_die( 'Unauthorized', 403 );
    }
    
    if ( function_exists( 'opcache_reset' ) ) {
        opcache_reset();
        header( 'Content-Type: application/json' );
        echo json_encode( [
            'status' => 'success',
            'message' => 'OPcache cleared',
            'timestamp' => time(),
        ] );
        exit;
    }
    
    wp_die( 'OPcache not available', 500 );
} );

В wp-config.php добавляем секретный токен:

define( 'CUSTOMCODE_OPCACHE_SECRET', 'your-very-secret-token-here' );

В GitHub Actions workflow добавляем вызов после деплоя:

- name: Clear OPcache
  run: |
    curl -s "https://custom-code.ru/?opcache_reset=${{ secrets.OPCACHE_SECRET }}"

Метод 3: Перезагрузка PHP-FPM

Самый радикальный способ — полная перезагрузка пула FPM. OPcache хранится в общей памяти процесса, поэтому при перезапуске кэш полностью очищается:

sudo systemctl reload php8.2-fpm

Этот метод надёжен, но вызывает кратковременный всплеск нагрузки, так как все worker-процессы пересоздаются. Подходит для ночных деплоев, но не для продакшна в часы пик.

Шаг 5: OPcache Preloading (PHP 7.4+)

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

Создание preload-скрипта:

Создаём файл /var/www/wordpress/opcache-preload.php:

<?php
/**
 * OPcache Preload для WordPress
 * Загружает ядро и активные плагины в память при старте FPM
 */

// Загружаем конфигурацию WordPress
require_once __DIR__ . '/wp-load.php';

// Список файлов для предзагрузки
$preload_files = [
    // Ядро WordPress
    __DIR__ . '/wp-includes/load.php',
    __DIR__ . '/wp-includes/functions.php',
    __DIR__ . '/wp-includes/class-wp.php',
    __DIR__ . '/wp-includes/class-wp-query.php',
    __DIR__ . '/wp-includes/post.php',
    __DIR__ . '/wp-includes/query.php',
    
    // Критичные плагины (замените на свои)
    __DIR__ . '/wp-content/plugins/redis-cache/redis-cache.php',
    __DIR__ . '/wp-content/plugins/query-monitor/query-monitor.php',
    
    // Функции темы
    __DIR__ . '/wp-content/themes/customcode/functions.php',
];

foreach ( $preload_files as $file ) {
    if ( file_exists( $file ) ) {
        opcache_compile_file( $file );
    }
}

Активация в php.ini:

opcache.preload = /var/www/wordpress/opcache-preload.php
opcache.preload_user = www-data

⚠️ Важно: Preloading работает только для PHP-FPM, не для CLI. Изменения в preload-скрипте требуют полной перезагрузки FPM. На высоконагруженных сайтах preloading даёт дополнительные 5-10% прироста производительности.

Шаг 6: OPcache GUI для визуального мониторинга

Для наглядного отображения статуса OPcache используется opcache-gui — лёгкий веб-интерфейс на одной странице.

Установка:

# Скачиваем в защищённую директорию
sudo mkdir -p /var/www/opcache-gui
cd /var/www/opcache-gui
sudo wget https://raw.githubusercontent.com/amnuts/opcache-gui/master/index.php

# Настраиваем доступ только по IP администратора
sudo chown -R www-data:www-data /var/www/opcache-gui

Защита через Nginx:

GUI содержит детальную информацию о сервере, поэтому доступ должен быть ограничен:

location /opcache-gui/ {
    alias /var/www/opcache-gui/;
    index index.php;
    
    # Только ваши IP
    allow 1.2.3.4;     # IP офиса
    allow 5.6.7.8;     # IP домашнего интернета
    deny all;
    
    # Базовая авторизация
    auth_basic "OPcache GUI";
    auth_basic_user_file /etc/nginx/.htpasswd;
    
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $request_filename;
    }
}

После настройки GUI доступен по адресу https://custom-code.ru/opcache-gui/ и показывает графики hit rate, использования памяти и список закэшированных файлов.

Шаг 7: Сравнение OPcache с другими кэшами

OPcache — лишь один из слоёв кэширования в стеке WordPress. Понимание различий помогает избежать путаницы:

Тип кэша Что кэширует Экономия
OPcache Скомпилированный PHP байт-код 2-3× ускорение PHP
Redis Object Cache Результаты SQL-запросов, опции, transient’ы 10-100× меньше запросов к БД
FastCGI Cache Готовый HTML-вывод страниц Полное исключение PHP для закэшированных страниц
Browser Cache CSS, JS, изображения в браузере пользователя Уменьшение трафика
CDN Cache Статика на edge-серверах Снижение нагрузки на origin

Все эти кэши дополняют друг друга. Например, при запросе к закэшированной странице FastCGI Cache возвращает HTML вообще без запуска PHP. При промахе мимо FastCGI Cache запускается PHP, который использует OPcache для быстрого выполнения кода и Redis для кэширования результатов SQL-запросов.

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

  • ✅ OPcache включён в php.ini (opcache.enable = 1)
  • memory_consumption установлен в 256-512 МБ (рассчитан по количеству файлов)
  • max_accelerated_files превышает количество PHP-файлов в проекте
  • revalidate_freq установлен в 60 секунд для продакшна
  • enable_cli = 1 для работы OPcache в WP-CLI и cron
  • ✅ Настроен скрипт мониторинга hit rate с алертами
  • ✅ Автоматическая инвалидация при деплое (через opcache_reset())
  • ✅ Рассмотрен вариант с preloading для критичных файлов
  • ✅ Hit rate стабильно выше 99%

Решение частых проблем

Hit rate низкий (меньше 95%)

Три возможные причины:

  1. Недостаточно памяти: увеличьте opcache.memory_consumption. Проверьте через GUI, сколько памяти свободно — если меньше 20%, нужно расширять.
  2. Недостаточно слотов для файлов: увеличьте opcache.max_accelerated_files. Если в проекте 10 000 PHP-файлов, а лимит 4000, часть файлов не будет кэшироваться.
  3. Слишком частая инвалидация: если revalidate_freq = 0, OPcache проверяет timestamp каждого файла при каждом запросе, что создаёт нагрузку на диск.

После деплоя изменения не видны

Если revalidate_freq = 60, OPcache будет использовать старый байт-код ещё минуту после обновления файлов. Решения:

  • Добавить opcache_reset() в скрипт деплоя
  • Перезагрузить PHP-FPM после деплоя
  • Временно установить revalidate_freq = 2 на время активных разработок

OPcache вызывает ошибки «Cannot redeclare class»

Это признак конфликта при перекомпиляции файлов. Обычно возникает при:

  • Использовании eval() или create_function() — такие конструкции плохо работают с OPcache
  • Условных объявлениях классов (например, if ( ! class_exists() )) — при инвалидации кэша класс может быть объявлен дважды

Решение: рефакторинг кода с использованием автозагрузки PSR-4 вместо ручных require_once.

OPcache потребляет слишком много памяти

Если memory_consumption установлен в 1 ГБ, но реально используется только 100 МБ, это пустая трата RAM. Проверьте фактическое использование через GUI и уменьшите параметр с запасом 30-50% на рост проекта.

WP-CLI работает медленно, несмотря на OPcache

По умолчанию opcache.enable_cli отключён, поэтому WP-CLI не использует OPcache. Включение этого параметра ускоряет выполнение WP-CLI команд в 2-3 раза, что критично для скриптов автоматизации, массовых операций и cron-задач.

Итог

OPcache — обязательный компонент современного PHP-стека. Правильная настройка занимает 15 минут, а выигрыш в производительности колоссален. В связке с Redis Object Cache и FastCGI Cache он образует трёхуровневую систему кэширования, которая позволяет WordPress обрабатывать сотни запросов в секунду даже на скромных VPS. Регулярный мониторинг hit rate и автоматическая инвалидация при деплоях гарантируют, что кэш работает эффективно и не становится источником проблем с устаревшим кодом.