Как PHP OPcache убивает production Bitrix — и три строки, которые это чинят
Три месяца мы смотрели в slow query log. Перекраивали индексы. Добавляли Redis-кеш там, где его раньше не было. Сайт становился чуть лучше, но страницы каталога иногда виснули на 800-900ms без видимой причины.
Потом случайно открыли opcache_get_status() прямо в PHP-скрипте. Увидели: opcache_hit_rate — 61%.
При нормально работающем кеше он должен быть выше 95%.
20 минут правки php.ini и двукратное падение p95. SQL-запросы мы так и не трогали.
Почему OPcache и Bitrix работают неожиданно
OPcache хранит скомпилированный байт-код PHP-файлов в памяти. При хит-рейте 61% почти каждый третий запрос к PHP-файлу идёт не из памяти, а с диска. Bitrix — это тысячи PHP-файлов в ядре. Каждый запрос к странице каталога проходит через сотни файлов модулей, компонентов, шаблонов.
Дефолтный конфиг OPcache писался под PHP 7 и небольшие приложения. У Bitrix своя специфика: ядро само по себе весит больше, чем большинство фреймворков. Дефолт этой специфики не учитывает.
Симптомы размытые. Страницы иногда медленные, иногда нет. Под нагрузкой всё хуже. Команда смотрит в MySQL, в nginx, в очередь задач — и не видит очевидного: PHP-слой работает в половину силы.
Главный индикатор: hit ratio ниже 95%
Первый шаг — получить текущее состояние кеша. Создайте временный файл на сервере:
<?php
$status = opcache_get_status();
echo 'Hit rate: ' . round($status['opcache_statistics']['opcache_hit_rate'], 2) . '%' . PHP_EOL;
echo 'Cached files: ' . $status['opcache_statistics']['num_cached_scripts'] . PHP_EOL;
echo 'Memory used: ' . round($status['memory_usage']['used_memory'] / 1024 / 1024) . 'M' . PHP_EOL;
echo 'Memory free: ' . round($status['memory_usage']['free_memory'] / 1024 / 1024) . 'M' . PHP_EOL;
echo 'Oom restarts: ' . $status['opcache_statistics']['oom_restarts'] . PHP_EOL;
Если opcache_hit_rate ниже 95% — у вас проблема с конфигурацией. Если oom_restarts больше нуля — OPcache сбрасывает кеш из-за нехватки памяти. Это создаёт нестабильность: после рестарта первые запросы идут с диска, производительность проседает, потом восстанавливается. Команда решает, что виноват «нестабильный» сервер.
opcache.memory_consumption: почему дефолтные 128M мало
Дефолт opcache.memory_consumption = 128 задан с запасом для обычного PHP-приложения. Bitrix — не обычное приложение.
Свежая установка Bitrix 23.x содержит 8000-12000 PHP-файлов только в ядре (/bitrix/modules). Добавьте файлы вашего кастомного кода, установленных модулей, шаблонов. На нашем проекте было 9400 файлов только в /bitrix/.
Каждый скомпилированный файл занимает в среднем 8-25 КБ в памяти OPcache. Простая математика: 9400 × 15 КБ ≈ 141 МБ. Это только ядро. При лимите в 128M кеш физически не вмещает всё ядро, начинает вытеснять файлы (eviction), при следующем запросе компилирует снова — и так по кругу.
Для Bitrix на production минимум — 256M. На нагруженных проектах стоит смотреть в сторону 512M.
opcache.memory_consumption = 256
opcache.max_accelerated_files: считаем файлы Bitrix ядра
Второй параметр, который часто игнорируют. Дефолт — 10000 или даже 4000 (зависит от дистрибутива). При 9400+ файлах на проекте это граничный случай, а у некоторых конфигураций ещё и меньше.
Узнать реальное количество файлов:
find /path/to/bitrix -name "*.php" | wc -l
Значение opcache.max_accelerated_files должно быть больше этого числа. Обязательно с запасом — на рост кодовой базы и кастомные модули.
opcache.max_accelerated_files = 20000
Число должно быть простым — OPcache использует хеш-таблицу, и простое число как размер снижает количество коллизий. Подходящие значения: 7963, 16229, 20011.
validate_timestamps=0 на проде — страшно, но правильно
opcache.validate_timestamps по умолчанию равен 1. Это значит: при каждом обращении к файлу PHP проверяет, не изменился ли он на диске. Даже если файл закеширован в OPcache.
На production это лишняя работа. Файлы ядра Bitrix не меняются между деплоями. Ваш кастомный код тоже не меняется в процессе работы сайта.
При validate_timestamps = 0 OPcache перестаёт проверять временны́е метки и отдаёт кешированный байт-код напрямую. Единственное ограничение: после деплоя нужно сбрасывать кеш вручную через opcache_reset() или рестарт PHP-FPM.
opcache.validate_timestamps = 0
Если пугает перспектива «забыть сбросить кеш» — добавьте opcache_reset() в деплой-скрипт. Одна строка.
Три строки, которые меняют ситуацию
Итоговый минимальный набор для Bitrix на production:
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20011
opcache.validate_timestamps = 0
После изменения — рестарт PHP-FPM (systemctl restart php8.1-fpm), затем проверяем opcache_get_status() снова. Если opcache_hit_rate не поднялся выше 90% — смотрим oom_restarts и num_cached_scripts. Возможно, нужно ещё увеличить memory_consumption.
На нашем проекте после этих трёх строк hit rate поднялся с 61% до 98%.
Что измерили после
Сервер: 4 ядра, 8 GB RAM, PHP 8.1, Bitrix магазин ~1500 DAU.
До:
opcache_hit_rate: 61%- p95 page time (каталог): ~900 ms
oom_restartsза сутки: 12
После:
opcache_hit_rate: 98%- p95 page time (каталог): ~420 ms
oom_restartsза сутки: 0
SQL-запросы не менялись. Nginx-конфиг не менялся. Только три строки в php.ini.
Итог
OPcache — это слой, про который вспоминают последним. А надо первым.
Если у вас Bitrix тормозит на production — до SQL и nginx проверьте opcache_get_status(). Пять секунд на чтение вывода, и вы уже знаете: это PHP-слой или нет. Дальше — в зависимости от ответа.
Про то, как искать узкие места в самом Bitrix дальше, — в статье Bitrix не тормозит. Тормозит то, как с ним работают. А про cache-warming через Cron как второй уровень защиты — Cache-warming через Cron.