Многоуровневый кеш в инфраструктуре: где ломается
Браузер, CDN, Nginx, Redis и кеш приложения ускоряют систему только при согласованных правилах. Разбираем, почему старые данные переживают релиз, запросы умножаются под нагрузкой и где искать источник рассинхронизации.

Многоуровневый кеш ускоряет ответ, но добавляет системе несколько независимых решений о том, что считать свежими данными. Браузер может показать старую карточку, CDN — вернуть собственную копию, Nginx — не обратиться к приложению, а Redis — сохранить устаревший объект после релиза. В статье разберём инфраструктуру на базе Nginx 1.27.x и Redis 7.x: где возникают рассинхронизация, cache stampede и ошибки с ключами, какие метрики собирать и что проверить до выхода в прод.
Из чего складывается многоуровневый кеш
У каждого слоя своя цель и свой срок жизни. Чем ближе кеш к пользователю, тем дешевле запрос для вашей инфраструктуры, но тем труднее быстро отозвать уже выданный ответ.
| Слой | Что хранит | Главный выигрыш | Типичный риск |
|---|---|---|---|
| Браузер | Статические файлы и HTTP-ответы | Запрос не доходит до сети | Пользователь видит старую версию |
| CDN | Копии ответов на edge-узлах | Меньше задержка и трафик до origin | Очистка не происходит одновременно во всех точках |
| Nginx | Ответы reverse proxy | Снимает повторные запросы с приложения | Ключ не учитывает заголовок или cookie |
| Кеш приложения | Результаты вычислений и запросов | Не нужно повторять дорогую логику | Данные устарели после изменения источника |
| Redis | Значения по ключам с TTL | Быстрый общий кеш для нескольких экземпляров | Промахи одновременно нагружают базу |
TTL — time to live, время жизни записи. После его окончания запись считается истёкшей, но конкретный слой может удалить её сразу, проверить при следующем чтении или временно отдать устаревшее значение, если это разрешено политикой.
Кеш — это не просто ускоритель. Это ещё один источник состояния, а значит, у него должны быть владелец, правила обновления и наблюдаемость.
Главная ловушка: одинаковый объект имеет разные ключи
Кеш отвечает не на вопрос что лежит в базе, а на вопрос какой ответ соответствует конкретному ключу. Если Nginx различает URL, но приложение различает ещё язык, регион, роль пользователя и версию схемы ответа, внешнему слою нельзя бездумно отдавать одну копию всем.
Особенно опасны cookies и заголовки. Ответ для авторизованного пользователя не должен попасть в общий кеш из-за неверной политики приватности. Заголовок Accept-Language может менять тело ответа, а Accept-Encoding — способ сжатия. Если эти параметры участвуют в формировании ответа, они должны учитываться в правилах кеширования либо запросы нужно разделить по маршрутам.
Ключ должен описывать все влияющие параметры
- Определите, какие поля запроса меняют ответ: путь, query-параметры, язык, регион, роль и версию схемы.
- Уберите из ключа шум: порядок query-параметров, трекинговые метки и значения, которые не влияют на результат.
- Разделите публичные и персональные ответы. Для персональных данных безопаснее отказаться от общего кеша, чем пытаться компенсировать ошибку очисткой.
- Добавьте версию формата в ключ, если после релиза структура значения меняется.
Проверка проста: возьмите два запроса, которые должны вернуть разные ответы, и сравните не только URL, но и заголовки, cookies, тело и ключ на каждом слое. Если ключи совпали, а ответы должны различаться, проблема уже найдена.
Устаревание: почему очистка одного слоя не помогает
Представьте цепочку: CDN хранит ответ 60 секунд, Nginx — 30 секунд, Redis — 10 минут. После изменения записи в базе приложение может получить новое значение из базы, но CDN продолжит отдавать старый HTTP-ответ ещё минуту. Если браузер получил Cache-Control с большим max-age, пользователь не увидит обновление даже после очистки CDN.
Частая ошибка — считать TTL единственным механизмом актуальности. TTL ограничивает срок, но не сообщает кешу, что конкретный объект изменился. Для каталогов, профилей и конфигурации обычно нужны явное удаление по ключу, смена версии ключа или короткий срок жизни. Универсального безопасного значения нет: его выбирают по допустимому возрасту данных и стоимости промаха.
Инвалидация не должна зависеть от одного запроса
Если запись изменяется через несколько сервисов, каждый из них должен одинаково обрабатывать событие изменения. Иначе один путь очистит Redis, но не CDN, а другой обновит базу без уведомления кешей. События должны быть повторяемыми: повторная очистка того же ключа не должна ломать систему.
Для больших наборов данных полезен версионный ключ. Вместо попытки удалить миллионы записей приложение меняет версию пространства имён, например product:v42. Старые ключи исчезают по TTL. Цена подхода — временный рост памяти и холодный кеш после переключения.
Cache stampede: когда промахи становятся аварией
Если популярная запись истекла, десятки или тысячи запросов одновременно видят промах и идут в базу за одним и тем же объектом. Внешне это выглядит как внезапный рост запросов к PostgreSQL, задержек и соединений, хотя причина находится в кеше.
Здесь применяют несколько защит, но они решают разные задачи:
- Single flight — только один запрос пересчитывает значение, остальные ждут его результат. Это снижает дублирование, но увеличивает задержку ожидающих запросов.
- Jitter для TTL — к сроку жизни добавляется небольшое случайное отклонение. Записи не истекают одновременно, но способ не спасает один сверхпопулярный ключ.
- Stale-while-revalidate — на короткое время отдаётся старая копия, пока фоновой процесс получает новую. Пользователь получает быстрый ответ, зато нужно явно принять допустимость устаревших данных.
- Прогрев — важные ключи заполняются до переключения трафика или после деплоя. Это расходует вычислительные ресурсы заранее и требует списка действительно горячих объектов.
Не включайте все механизмы одновременно без измерений. Например, stale-while-revalidate может скрыть проблему источника, а блокировка на один ключ — создать очередь, если пересчёт занимает секунды.
Где подход ломается
Разные правила свежести
Публичная страница может безопасно жить в CDN несколько минут, а остаток товара — уже нет. Если оба ответа проходят через одинаковую политику, бизнес-правило проигрывает удобству кеширования. Разделяйте маршруты по цене ошибки: для каталога допустима задержка, для платежного статуса — обычно нет.
Неожиданные ответы об ошибках
Кеширование ответов 404, 500 и 429 иногда полезно, но только с коротким и осознанным TTL. Случайная ошибка приложения, сохранённая на edge-узле, превращается в устойчивый сбой для всех пользователей. Проверяйте, какие коды ответа кешируются Nginx и CDN, а также есть ли отдельные правила для ошибок.
Релиз меняет структуру значения
Старый экземпляр приложения может прочитать ключ, записанный новой версией, и упасть из-за другого формата. Это особенно вероятно при поэтапном деплое. Используйте совместимый формат на время перехода или версию ключа, а удаление старого пространства планируйте после завершения миграции.
Локальный кеш маскирует проблему
Один экземпляр приложения может иметь локальную копию, другой — уже обновлённую запись Redis. Пользователь получает разные ответы в зависимости от маршрута балансировщика. Локальный кеш стоит применять только для данных, где такая рассинхронизация допустима, и ограничивать его размер и TTL.
Что измерять, а не угадывать
Сравнивать систему до и после изменения нужно в одинаковых условиях: одна версия приложения, тот же объём данных, одинаковая доля чтения и зафиксированное число параллельных запросов. Если таких замеров нет, не называйте оптимизацию ускорением — сначала соберите базовую линию.
- Hit ratio по каждому слою, а не только общий процент попаданий.
- p50, p95 и p99 задержки отдельно для попаданий и промахов.
- Число запросов к базе на один пользовательский запрос.
- Количество одновременных пересчётов одного ключа.
- Размер кеша, долю вытеснений, ошибки соединения и время очистки.
- Возраст возвращаемых данных, если источник позволяет его определить.
Логи должны показывать причину ответа: cache hit, miss, stale, bypass или error. Не записывайте в них персональные значения и содержимое токенов. Для диагностики достаточно идентификатора ключа в безопасной форме, слоя и времени обработки.
Следующий шаг: составьте карту одного горячего маршрута
Не начинайте с добавления ещё одного Redis. Возьмите один endpoint с высокой нагрузкой и нарисуйте путь запроса от браузера до базы: где формируется ключ, кто владеет TTL, что происходит при изменении данных и какой слой очищается первым.
- Зафиксируйте текущие hit ratio, p95, число запросов к базе и объём памяти.
- Проверьте два запроса с разными языками, ролями и query-параметрами, если они влияют на ответ.
- Смоделируйте истечение популярного ключа и посчитайте параллельные пересчёты.
- Измените одну запись и измерьте время до появления нового значения через каждый слой.
- Добавьте алерт на рост промахов, задержек и запросов к источнику, затем повторите тест после релиза.
Инвалидация CDN — отдельная тема; здесь важнее сначала доказать, какой слой отдаёт устаревший ответ. После этого уже выбирайте между коротким TTL, версионными ключами, фоновым обновлением и полным отказом от кеша для конкретного маршрута.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: Nginx documentation: proxy caching, Redis documentation: cache-aside pattern
Частые вопросы
Нужно ли кешировать данные на всех уровнях одновременно?
Как понять, какой слой вернул старый ответ?
Что делать, если после истечения TTL база получает всплеск запросов?
Можно ли хранить в общем кеше ответы авторизованных пользователей?
Что изменится при переходе на другую версию Nginx или Redis?
Что делать, если очистка кеша не сработала?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.