Кеширование в PHP: чек-лист перед выкаткой в прод
Чек-лист по кешированию в PHP: где OPcache молчит про промахи, почему file_get_contents на кеше опаснее APCu, как считать hit rate и не отдать протухшие данные всему трафику разом.

Кеш в PHP — это не «включил APCu, стало быстрее». Это набор решений, каждое из которых при неверной настройке тихо ломается под нагрузкой: OPcache не подхватывает новый код после деплоя, APCu живёт только внутри одного FPM-процесса, файловый кеш держит блокировку и превращает диск в узкое место. Ниже — чек-лист, который стоит пройти до того, как продакшн начнёт отдавать протухшие данные или падать на инвалидации. Ориентир — PHP 8.1–8.3 и php-fpm.
Разделите три уровня кеша — их путают чаще всего
«Кеширование в PHP» — это минимум три разные вещи с разным жизненным циклом. Смешивание уровней в голове ведёт к настройкам, которые противоречат друг другу.
- OPcache — кеш скомпилированного байткода. Не имеет отношения к вашим данным. Отвечает на вопрос «сколько раз PHP парсит один и тот же .php-файл».
- Кеш данных в памяти — APCu (локальный, на процесс/машину) и Redis/Memcached (общий, по сети). Здесь живут результаты запросов, сериализованные объекты, счётчики.
- Кеш HTTP-ответов — заголовки
Cache-Control,ETag, обратный прокси (Nginx, Varnish, CDN). До PHP запрос может вообще не дойти.
Дальше — по каждому уровню, что проверить.
OPcache: код в проде компилируется один раз
OPcache включён по умолчанию в сборках PHP, но настройки по умолчанию рассчитаны на dev. В проде их надо пересмотреть.
- opcache.enable=1 и opcache.enable_cli — второй обычно оставляют выключенным, кроме случаев с долгоживущими CLI-воркерами.
- opcache.validate_timestamps. В проде ставят
0: PHP перестаёт проверять mtime файлов на каждый запрос. Цена — после деплоя надо явно сбросить OPcache (перезапуск php-fpm или сброс через управляющий эндпоинт), иначе крутится старый код. - opcache.memory_consumption. Если памяти мало, часть скриптов вытесняется и перекомпилируется — виден рост CPU. Смотрите занятость через
opcache_get_status(). - opcache.max_accelerated_files. Значение по умолчанию мало для крупного фреймворка с тысячами файлов автолоада. Если файлов больше лимита, лишние не кешируются.
Как проверить, что OPcache реально работает и не переполнен:
<?php
// opcache-check.php — закройте доступ извне после проверки
$status = opcache_get_status(false);
$stats = $status['opcache_statistics'];
$hits = $stats['hits'];
$misses = $stats['misses'];
$rate = $hits + $misses > 0
? round($hits / ($hits + $misses) * 100, 2)
: 0;
// Промахи после прогрева = скрипт не в кеше или кеш переполнен
printf("hit rate: %s%%\n", $rate);
printf("cached scripts: %d / %d\n",
$stats['num_cached_scripts'],
ini_get('opcache.max_accelerated_files'));
printf("restarts (oom): %d\n", $stats['oom_restarts']);
Если oom_restarts больше нуля — памяти OPcache не хватает, кеш периодически полностью сбрасывается. Это дороже, чем кажется: каждый сброс = массовая перекомпиляция под трафиком.
Самая частая проблема с OPcache в проде — не производительность, а деплой: выкатили новый код, а validate_timestamps=0 продолжает отдавать старый байткод. Сброс OPcache должен быть шагом деплоя, а не тем, о чём вспоминают после инцидента.
APCu против Redis: не путайте локальный кеш с общим
APCu хранит данные в разделяемой памяти на одной машине. Это его сила (нет сети, доступ за микросекунды) и его ограничение (данные не видны другому серверу и не переживают перезапуск php-fpm). Redis — по сети, общий для всего кластера, переживает рестарт приложения.
| Критерий | APCu | Redis / Memcached |
|---|---|---|
| Область видимости | Один сервер (по факту — общая для FPM-пула) | Весь кластер по сети |
| Задержка доступа | Микросекунды, без сети | Сеть + сериализация |
| Переживает рестарт FPM | Нет | Да |
| Инвалидация на всех нодах | Нет, каждая нода отдельно | Да, из одной точки |
| Типичная роль | Горячий локальный кеш, конфиг, метаданные | Сессии, общие данные, очереди |
Ошибка чек-листа: держать в APCu данные, которые обязаны быть согласованы между серверами (например, признак блокировки пользователя). На двух нодах будут разные значения, и вы получите поведение, которое не воспроизводится.
Обратная ошибка: гонять через Redis то, что читается тысячи раз в секунду и одинаково на каждой ноде (справочники, скомпилированный конфиг). Сетевой round-trip на такой нагрузке съест выигрыш.
Файловый кеш — только когда нет альтернативы
Файловый кеш (стандартный fallback многих фреймворков) кажется бесплатным, но под конкурентной записью упирается в блокировки и в производительность диска. На загруженном проекте один популярный ключ, который часто перезаписывается, превращается в точку сериализации запросов. Если выбираете файловый кеш «на время» — заложите переход на APCu или Redis заранее.
Инвалидация: где чаще всего теряют данные
Инвалидация — самая тонкая часть. Проверьте по пунктам:
- У каждого ключа есть TTL. Кеш без срока жизни рано или поздно отдаёт мусор. TTL — страховка на случай, если явная инвалидация не сработает.
- Ключи предсказуемы и версионируемы. Добавляйте версию в префикс:
catalog:v3:product:42. Сменили формат хранимых данных — подняли версию, старые записи умрут по TTL сами, без ручной чистки. - Нет cache stampede. Когда популярный ключ протухает, сотни запросов одновременно бегут пересчитывать одно и то же. Об этом отдельно ниже.
- Инвалидация не завязана на один сервер. Если пишете в APCu на ноде A, нода B об этом не узнает — см. таблицу выше.
Cache stampede: защита от одновременного пересчёта
Прием — probabilistic early expiration: пересчитывать значение чуть раньше срока и не всеми запросами сразу, а с вероятностью. Схематично на APCu:
<?php
// PHP 8.x, расширение apcu
function cacheRemember(string $key, int $ttl, callable $compute): mixed
{
$meta = apcu_fetch($key, $found);
$now = time();
// Есть значение и до истечения ещё далеко — отдаём как есть
if ($found && $now < $meta['soft_expire']) {
return $meta['value'];
}
// Окно перед истечением: пересчитывает только часть запросов.
// Чем ближе к истечению, тем выше шанс пересчёта.
if ($found) {
$window = $meta['hard_expire'] - $meta['soft_expire'];
$left = $meta['hard_expire'] - $now;
if ($left > 0 && mt_rand(0, $window) < $left) {
return $meta['value']; // ещё рано, отдаём старое
}
}
$value = $compute();
apcu_store($key, [
'value' => $value,
'soft_expire' => $now + (int) ($ttl * 0.9),
'hard_expire' => $now + $ttl,
], $ttl + 5); // страховочный TTL самого ключа чуть больше
return $value;
}
Идея: значение живёт до hard_expire, но в последние 10% срока запросы с растущей вероятностью начинают пересчитывать его заранее — по одному, а не все разом. Полноценная защита от stampede в кластере требует ещё и блокировки на Redis (SET key val NX PX), чтобы пересчитывал один клиент. Блокировки на Redis — отдельная тема, здесь показан только локальный механизм.
HTTP-кеш: до PHP запрос может не дойти
Самый дешёвый кеш — тот, при котором PHP вообще не запускается. Проверьте:
- Cache-Control выставлен осознанно. Для статики и редко меняющихся ответов —
public, max-age=.... Для персональных данных —private, no-store, иначе прокси или CDN закешируют чужой ответ. - ETag или Last-Modified для условных запросов. Позволяют отдавать
304 Not Modifiedбез тела. - Сессионные страницы не попадают в общий кеш. Классическая утечка: страница с
Set-Cookieзакешировалась на CDN и раздаётся всем с чужой сессией.
Что мерить, а не угадывать
Кеш без метрик — вера, а не инженерия. Минимальный набор показателей:
- Hit rate по каждому слою. OPcache — через
opcache_get_status(), Redis — черезINFO stats(keyspace_hits/keyspace_misses). Низкий hit rate после прогрева означает, что ключи не переиспользуются или слишком быстро вытесняются. - Вытеснения (evictions). Если Redis настроен с
maxmemoryи политикой вытеснения, растущие evictions = памяти мало, кеш работает как проходной двор. - Латентность до и после. Померьте время ответа эндпоинта с холодным и прогретым кешем на одинаковой нагрузке — так вы увидите реальный выигрыш, а не предполагаемый.
Финальный чек-лист перед выкаткой
- OPcache включён,
validate_timestamps=0, сброс кеша встроен в деплой. oom_restarts= 0, лимитmax_accelerated_filesпокрывает все файлы проекта.- APCu — только для локальных, одинаковых на всех нодах данных.
- Общие и согласованные данные — в Redis/Memcached, не в APCu.
- У каждого ключа есть TTL и версия в префиксе.
- Есть защита от stampede на горячих ключах.
- HTTP-ответы с персональными данными помечены
private/no-store. - Настроены метрики hit rate и evictions по каждому слою.
Следующий шаг: снимите базовую линию латентности до включения кеша, затем сравните после прогрева на боевой нагрузке. Без этой пары чисел вы не сможете сказать, помогает кеш или просто маскирует проблему, которую стоило решить в запросах к базе.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: PHP Manual: OPcache configuration, PHP Manual: APCu functions
Частые вопросы
Нужен ли Redis, если хватает APCu?
Почему после деплоя сайт отдаёт старый код?
А если у меня PHP 7.4, чек-лист подойдёт?
Что делать, если hit rate низкий даже после прогрева?
Опасно ли ставить validate_timestamps=0 на проде?
Можно ли обойтись без защиты от stampede?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.