В какой-то момент любой популярный PHP‑проект упирается не в CPU, а в количество одинаковых запросов. Особенно «болит», когда вы сидите на одном VDS и приходится одновременно крутить PHP‑FPM, Nginx, базу данных и очереди. Здесь на сцену выходят кеши: CDN, proxy_cache в Nginx и Redis.
Цель статьи — показать, как собрать понятную многоуровневую схему кеширования вокруг PHP‑приложения, чтобы:
- уменьшить нагрузку на PHP‑FPM и базу;
- сделать сайт быстрее для пользователей из разных регионов;
- избежать классических грабель с устаревшими данными и сломанными авторизациями.
Будем говорить с прицелом на типовой стек: VDS + Nginx + PHP‑FPM + Redis и внешний CDN (Cloudflare, RuCDN, VK Cloud и т.п.).
Зачем вообще многоуровневый кеш: слои и роли
Удобно думать о кешировании как о нескольких слоях, которые стоят между пользователем и PHP:
- Браузер и CDN — верхний уровень, ближе всего к пользователю.
- Nginx
proxy_cache(или fastcgi-кеш) — кеш на стороне вашего сервера. - Redis — кеш для самого PHP‑приложения (объекты, фрагменты, сессии).
Каждый уровень решает свою задачу и опирается на корректные заголовки Cache-Control, Expires, ETag, Last-Modified, Vary и т.д. Ошибка на одном уровне может сделать бесполезным все остальные.
CDN и Nginx кешируют целые HTTP‑ответы, Redis — данные и фрагменты внутри приложения. Не пытайтесь любым способом решать все задачи на одном уровне.
Базовая архитектура: как разложить ответственность
Типичная схема для VDS с Nginx и PHP такая:
- CDN стоит перед Nginx, забирает статику и часть HTML‑страниц по
Cache-Control; - Nginx крутится на VDS, обслуживает статику, отдает PHP через
php-fpm, используетproxy_cache(илиfastcgi_cacheдля PHP); - PHP‑приложение использует Redis для кеша объектов, результатов запросов в БД и сессий.
При этом важно:
- научить приложение отдавать разумные заголовки кеширования;
- настроить Nginx так, чтобы он уважал эти заголовки и не ломал авторизацию;
- настроить CDN так, чтобы не кэшировать приватный контент и учитывать заголовки
Vary.
Такую архитектуру можно развернуть как на одном мощном VDS, так и на нескольких машинах, где под Nginx, PHP‑FPM и Redis выделены отдельные инстансы.

HTTP-кеш как фундамент: Cache-Control и друзья
Прежде чем трогать Nginx и Redis, имеет смысл навести порядок в том, какие HTTP‑заголовки отдает PHP‑приложение. Кеши верхних уровней по сути лишь интерпретируют то, что вы им скажете через заголовки.
Ключевые заголовки
Cache-Control— основной управляющий заголовок для браузеров, CDN и Nginx.Expires— старый, но все еще учитывается браузерами; лучше дублировать важные правила изCache-Control.ETagиLast-Modified— для условных запросов (If-None-Match,If-Modified-Since).Vary— подсказывает кешу, какие заголовки влияют на вариативность контента (например, язык, тип устройства).
Типичные режимы Cache-Control:
public, max-age=600— можно кешировать всеми, 10 минут;private, max-age=0, must-revalidate— только в браузере пользователя, только с переобновлением;no-store— вообще не кешировать (пароли, токены, чувствительные данные).
С PHP (без фреймворка) это может выглядеть так:
<?php
// Публичная кэширующаяся страница
header('Cache-Control: public, max-age=600');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
// Для условных запросов
$etag = 'W/"' . md5($content) . '"';
header('ETag: ' . $etag);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
http_response_code(304);
exit;
}
echo $content;
Во многих фреймворках (Symfony, Laravel и др.) эти заголовки конфигурируются через middleware/response-объект — важно использовать их централизованно, а не рассыпать header() по коду.
CDN + Nginx: как поделить обязанности по кешированию
CDN в идеале должен:
- забирать статику (CSS, JS, картинки, шрифты) с большим TTL;
- кэшировать определенные HTML‑страницы (лендинги, блог, каталоги) по
Cache-Control; - уважать
no-store,private,VaryиSet-Cookie.
Nginx при этом выполняет роль origin‑сервера с собственной proxy_cache, чтобы не дергать каждый раз PHP‑FPM и базу даже тогда, когда CDN пришел с пропущенным кешем или временно отключен.
Когда нужен Nginx proxy_cache, если есть CDN
Есть несколько типичных сценариев, когда локальный кеш в Nginx все равно нужен:
- CDN проксирует только статику, а HTML‑страницы идут напрямую;
- CDN не кэширует приватные или сложно-динамические страницы, но вы хотите иметь микрокеш хотя бы на несколько секунд для защиты от пиков;
- часть трафика идет мимо CDN (админка, интеграции, API‑клиенты, внутренние сервисы).
В этих случаях proxy_cache (или fastcgi_cache) в Nginx позволяет разгрузить PHP‑FPM даже без участия CDN.
Про более продвинутые схемы с map и split-кешем по кукам, языкам и типам устройств я писал отдельно в материале про кеш‑паттерны Nginx: динамика через SSI и сабзапросы с кешем.
Настройка Nginx proxy_cache для PHP‑сайта
Рассмотрим ситуацию, когда Nginx стоит перед PHP‑FPM, а мы хотим добавить кеш целых ответов для публичных страниц. Ниже — рабочий базовый пример, который легко адаптировать под свой проект.
Выбор proxy_cache vs fastcgi_cache
Если у вас Nginx общается с PHP через fastcgi_pass (PHP‑FPM), логичнее использовать fastcgi_cache. Но в ряде конфигураций PHP‑приложение прячут за отдельным backend‑сервером (php-backend:9000, uwsgi, другой Nginx), и тогда удобнее использовать proxy_cache.
Логика и принципы схожие, различается набор переменных и директив. В этой статье будем опираться на proxy_cache, но концептуально все то же применимо к fastcgi_cache.
Базовая конфигурация proxy_cache
proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=php_cache:100m max_size=10g inactive=60m use_temp_path=off;
map $request_method $purgeable {
default 0;
GET 1;
HEAD 1;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://php_backend;
proxy_cache php_cache;
proxy_cache_bypass $cookie_PHPSESSID;
proxy_no_cache $cookie_PHPSESSID;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status always;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Ключевые моменты:
keys_zone=php_cache:100m— память под индексы кеша (не под сами ответы);max_size=10g— предел кеша на диске, важно не забыть про место на VDS;proxy_cache_bypassиproxy_no_cacheпо cookie сессии — грубый, но работающий способ не кешировать персонализированный контент;proxy_cache_use_stale— позволяет отдавать устаревший кеш, если бэкенд временно недоступен.
Этот пример намеренно упрощен, чтобы показать базовую идею. В реальных проектах часто добавляют условия по Cache-Control, методам, заголовкам, URI и используют дополнительные зоны для разных типов трафика.
Учет заголовков Cache-Control в proxy_cache
По умолчанию proxy_cache довольно прямолинеен: смотрит на коды ответов и директивы proxy_cache_valid. Чтобы учитывать заголовки, которые вы расставляете из PHP, нужно чуть больше логики.
map $upstream_http_cache_control $no_cache {
default 0;
~*no-cache 1;
~*no-store 1;
~*private 1;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://php_backend;
proxy_cache php_cache;
proxy_cache_bypass $cookie_PHPSESSID $no_cache;
proxy_no_cache $cookie_PHPSESSID $no_cache;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid any 1m;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Теперь если PHP вернет Cache-Control: private, no-store, Nginx не будет сохранять этот ответ в кеш и не будет брать его из кеша.
Не ломайте семантику
Cache-Controlради удобства. Если ответ реально приватный — пусть будет приватным на всех уровнях.
Микрокеш для PHP: когда TTL в 5–10 секунд спасает от пиков
Один из самых эффективных приемов — микрокеш (microcaching): кешируем публичные страницы всего на несколько секунд, но за счет этого защищаем PHP‑FPM от волн трафика (например, при анонсе в соцсетях).
Пример микрокеша в Nginx:
proxy_cache_valid 200 301 302 10s;
proxy_cache_valid 404 1s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
Директива proxy_cache_lock не дает десяткам запросов одновременно «пробивать» кеш: первый запрос ждет ответа бэкенда и записывает его в кеш, остальные ждут итог и сразу получают закешированную копию.
Микрокеш хорошо работает для:
- лендингов;
- публичных новостей и статей;
- каталогов без персонализации;
- страниц со сложной выборкой из БД, но предсказуемым трафиком.
Но его лучше не включать для страниц с корзинами, личными кабинетами, персональными рекомендациями без аккуратной сегментации по cookie и session. Подробнее про TTL‑паттерны и защиту от пиков можно посмотреть в разборе HTTP-range и кеширования в Nginx/Apache.
Redis на уровне PHP: объектный кеш и сессии
Redis в PHP‑проектах обычно используют для трех задач:
- кеш результатов дорогих запросов к базе;
- кеш собранных DTO, моделей и фрагментов шаблонов;
- хранение сессий (вместо файловой системы или БД).
Плюсы Redis по сравнению с файловым кешем:
- низкая латентность при большом количестве мелких операций;
- удобное удаление по ключам и паттернам;
- возможность масштабирования и отказоустойчивости (Sentinel, Cluster);
- единый кросс‑node кеш, когда PHP‑FPM запущен на нескольких инстансах.
Простой пример кеша в Redis из PHP
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'article:' . $articleId;
$data = $redis->get($key);
if ($data === false) {
// эмулируем дорогой запрос в базу
$data = loadArticleFromDb($articleId);
$redis->setex($key, 600, json_encode($data));
}
$article = json_decode($data, true);
renderArticle($article);
Здесь TTL 600 секунд управляет временем жизни объекта в Redis. Важно продумать стратегию инвалидации: как вы будете сбрасывать кеш при изменении статей, товаров, профилей пользователей. Для более сложных кейсов с Redis и Memcached у нас есть отдельная статья: сравнение Memcached и Redis для PHP‑кеша.
Redis как сторедж сессий
Хранить сессии в Redis удобно, если PHP‑пул масштабируется по горизонтали. В PHP это обычно настраивается через session.save_handler и session.save_path или средствами фреймворка. Главное — не забывать про таймауты, очистку и политику maxmemory в самом Redis, чтобы сессии не выталкивались неожиданно.

Инвалидация кеша: CDN, Nginx, Redis
Самая сложная часть — не настроить кеш, а правильно его инвалидировать. У вас три уровня, и каждый живет своей жизнью. Ошибка инвалидации на одном уровне приводит к загадочным «фантомным» багам.
Инвалидация Redis
На уровне PHP проще всего: вы точно знаете, какие сущности поменялись, и можете удалить конкретные ключи:
<?php
function invalidateArticleCache(int $id, Redis $redis): void
{
$redis->del('article:' . $id);
}
Для связанных сущностей удобно хранить списки ключей в Redis или использовать именованные префиксы: user:123:profile, user:123:orders. При изменении пользователя удаляете все ключи с этим префиксом (через скрипт или фоновую задачу).
Инвалидация Nginx proxy_cache
Nginx не умеет из коробки «красиво» удалять отдельные ключи из кеша по HTTP‑запросу. Чаще всего используют сторонние модули или обходные пути:
- встроенный
proxy_cache_purgeиз некоторых патчей/билдов; - удаление файлов из кеш‑директории по ключу (требует знания структуры файлов);
- гибрид: уменьшенный TTL плюс
cache-bustingчерез query‑параметры или версии в URL.
На практике во многих проектах вместо «идеального» инвалидационного API выбирают стратегию:
- умеренные TTL (1–10 минут) для HTML;
- агрессивное версионирование статики (CSS, JS, картинки) в URL, чтобы избежать ручной очистки;
- четкое разделение кэширующихся и не кэширующихся URI.
Инвалидация CDN
У CDN есть два типичных механизма:
- Purge по URL или префиксу;
- cache-busting через версии в URL (пример:
/app.css?v=20250301или/assets/2025-03-01/app.css).
Лучше не злоупотреблять массовым purge всего кеша: под нагрузкой это способ устроить себе DoS, когда весь трафик одновременно идет в origin, то есть к вашему Nginx и PHP‑FPM.
Как сделать так, чтобы уровни кеша не мешали друг другу
Есть несколько базовых правил, которые сильно уменьшают количество боли при многоуровневом кешировании.
1. Разделяйте публичный и приватный контент на уровне URL
Если можно, выделите:
- отдельные пути или домены для API и админки, которые не кэшируются;
- отдельные пути под публичные страницы, которые можно смело кэшировать.
Простая схема:
/admin/— кеш отключен на всех уровнях;/api/— только короткий микрокеш или вовсе без кеша;/blog/,/catalog/— кешируются CDN и Nginx, Redis используется для части логики внутри.
2. Четко помечайте приватный контент
Для авторизованных пользователей и страниц с персонализацией используйте:
Cache-Control: private, max-age=0, must-revalidate;- отсутствие
publicиs-maxageв заголовке; - аккуратную работу с
Set-Cookie(многие CDN автоматически отключают кеш при наличии этого заголовка).
Это уменьшит риск того, что CDN или Nginx случайно закешируют страницу одного пользователя и отдадут её другому.
3. Используйте Vary там, где правда нужна вариативность
Если вы отдаете разный контент в зависимости от заголовков (например, язык или тип устройства), не забывайте Vary. Но не переусердствуйте: Vary: User-Agent может взорвать размер кеша. Лучше опираться на более грубые признаки (например, Vary: Accept-Language, Vary: X-Device-Type).
Типовые ошибки и анти‑паттерны
Из практики конфигураций на VDS можно выделить несколько самых частых ошибок, которые встречаются снова и снова.
1. Глобальный кеш всех 200‑ответов без учета cookie
Классическая «бомба» — конфиг вроде:
proxy_cache_valid 200 1h;
proxy_cache_bypass 0;
proxy_no_cache 0;
Такой конфиг быстро приводит к тому, что:
- страница личного кабинета одного пользователя оказывается в кеш‑слое и отдается всем остальным;
- корзины перестают обновляться;
- профили и балансы смешиваются;
- любые персонализированные блоки «залипают» и начинают вести себя хаотично.
Решение — учитывать Set-Cookie и Cookie хотя бы для ключевых сценариев, либо не кэшировать такие URI вообще.
2. Неверный расчет ресурсов под кеши на VDS
Распространенная проблема — ставят большой max_size для Nginx proxy_cache и большой Redis на одном диске и памяти в рамках VDS, а потом удивляются свопу и падению производительности.
Рекомендации:
- под Redis планировать память с запасом и ограничивать его
maxmemoryиmaxmemory-policy; - Nginx‑кеш держать на диске, а не в tmpfs, если вы не уверены в объеме RAM;
- регулярно смотреть метрики заполнения памяти и диска и алертить по порогам.
3. Отсутствие диагностики кеша
Без диагностических заголовков трудно понять, откуда конкретно пришел ответ: из CDN, Nginx или PHP.
В Nginx используйте как минимум:
add_header X-Cache-Status $upstream_cache_status always;
В CDN многие провайдеры позволяют добавить свой заголовок (например, X-CDN-Cache) и смотреть логи попаданий и промахов. В PHP можно добавить заголовок вида X-App-Cache с информацией о попадании в Redis‑кеш.
Пошаговый чек‑лист для внедрения кеша в PHP‑проект
Если вы только начинаете строить многоуровневый кеш для PHP‑проекта на VDS, можно идти по шагам.
- Навести порядок в HTTP‑заголовках из PHP:
- определить, какие разделы публичные, какие приватные;
- добавить корректный
Cache-Controlи, по возможности,ETag/Last-Modified; - обязателен аудит ответов для авторизованных пользователей.
- Включить Redis‑кеш на уровне приложения:
- кешировать самые дорогие запросы к базе;
- перевести сессии на Redis (при необходимости и наличии отказоустойчивости);
- описать и реализовать стратегию инвалидации.
- Добавить микрокеш в Nginx для публичных страниц:
- маленький TTL (5–15 секунд) для начала;
- ограничить кеш по URI и методам (только GET/HEAD);
- не кешировать
/admin,/apiи явные приватные зоны.
- Подключить CDN для статики и части HTML:
- постепенно включать кеширование HTML для безопасных разделов;
- настроить invalidation API и/или версионирование ресурсов;
- проверить работу
VaryиSet-Cookieчерез CDN.
- Настроить мониторинг и логи:
- логировать
$upstream_cache_statusв Nginx; - следить за hit/miss в CDN и Redis;
- выделить алерты на резкий рост miss и свопинга.
- логировать
Когда многоуровневый кеш не нужен (или вреден)
Иногда попытка прикрутить «все и сразу» только усложняет жизнь и увеличивает время отладки.
- внутренние административные панели без публичного трафика;
- API с жесткими требованиями к консистентности данных и без пиковых нагрузок;
- маленькие проекты, где главная проблема — не скорость, а недостаток функционала;
- системы, где основная боль — I/O или сложные внешние интеграции, а не CPU и PHP.
В таких случаях разумнее ограничиться Redis‑кешем на уровне приложения и простыми заголовками Cache-Control без агрессивного proxy_cache и сложных правил CDN.
Итоги
Грамотное использование кешей — это не «включить волшебный флажок», а аккуратная архитектура:
- PHP отвечает за правильные заголовки и объектный кеш в Redis;
- Nginx с
proxy_cacheилиfastcgi_cache— за микрокеш и разгрузку PHP‑FPM; - CDN — за глобальный кеш статики и частей HTML ближе к пользователю.
Важно не только настроить кеширование, но и продумать инвалидацию, мониторинг и ограничение ресурсов на VDS. Тогда многоуровневый стек PHP + Nginx + CDN + Redis реально даст заметный прирост производительности и устойчивости под нагрузкой, а не превратится в источник загадочных багов и «случайных» утечек приватного контента.


