Акция Панель управления ispmanager для VDS — первый месяц бесплатно
до 31.07.2026 Подробнее
Выберите продукт

PHP, Nginx, CDN и Redis: как собрать быструю многоуровневую систему кеширования

Разбираемся, как собрать внятную многоуровневую схему кеширования для PHP-сайта: CDN, Nginx proxy_cache или fastcgi_cache и Redis. Пошагово настраиваем HTTP-заголовки, TTL, микрокеш, инвалидацию и мониторинг так, чтобы разгрузить PHP‑FPM и базу на VDS, не сломав авторизацию и персонализацию.
PHP, Nginx, CDN и Redis: как собрать быструю многоуровневую систему кеширования

В какой-то момент любой популярный 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:

  1. Браузер и CDN — верхний уровень, ближе всего к пользователю.
  2. Nginx proxy_cache (или fastcgi-кеш) — кеш на стороне вашего сервера.
  3. 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 выделены отдельные инстансы.

Схема многоуровневого кеширования с участием браузера, CDN, Nginx и 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() по коду.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

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, чтобы сессии не выталкивались неожиданно.

Монитор с кодом PHP и 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, можно идти по шагам.

  1. Навести порядок в HTTP‑заголовках из PHP:
    • определить, какие разделы публичные, какие приватные;
    • добавить корректный Cache-Control и, по возможности, ETag/Last-Modified;
    • обязателен аудит ответов для авторизованных пользователей.
  2. Включить Redis‑кеш на уровне приложения:
    • кешировать самые дорогие запросы к базе;
    • перевести сессии на Redis (при необходимости и наличии отказоустойчивости);
    • описать и реализовать стратегию инвалидации.
  3. Добавить микрокеш в Nginx для публичных страниц:
    • маленький TTL (5–15 секунд) для начала;
    • ограничить кеш по URI и методам (только GET/HEAD);
    • не кешировать /admin, /api и явные приватные зоны.
  4. Подключить CDN для статики и части HTML:
    • постепенно включать кеширование HTML для безопасных разделов;
    • настроить invalidation API и/или версионирование ресурсов;
    • проверить работу Vary и Set-Cookie через CDN.
  5. Настроить мониторинг и логи:
    • логировать $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 реально даст заметный прирост производительности и устойчивости под нагрузкой, а не превратится в источник загадочных багов и «случайных» утечек приватного контента.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...