Назад в блог

Как 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.