awesome-everything EN
↑ Обратно к восхождению

Кеширование

Композиция стека кэшей: единая стратегия по CDN, прокси, Redis и БД

Суть Многоуровневый кэш корректен ровно настолько, насколько корректно компонуются его слои. Этот капстоун — сеньорский фреймворк: какой слой чем владеет, как каскадируются TTL, куда распространяется purge и как каждый слой деградирует при падении origin.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

25 декабря 2015 года, под рождественской DoS-атакой, Valve выкатила конфиг кэширования, который «некорректно кэшировал веб-трафик аутентифицированных пользователей». Около часа страницы Steam Store, собранные для одного залогиненного пользователя, кэшировались на краю и отдавались другим: адреса биллинга, история покупок, последние две цифры карты, последние четыре цифры телефона Steam Guard, email. Пострадало порядка 34 000 пользователей, прежде чем Valve выключила магазин. Приложение было корректно. Баг жил в шве между слоями: страница за логином, которая ни разу не сказала private, лежала в общем кэше, который решил, что может.

Каждый слой — это кэш; вопрос в том, кто чем владеет

К моменту, когда байт доходит до пользователя, он мог пройти четыре кэша: край CDN, reverse proxy (Varnish/nginx) перед приложением, кэш приложения (Redis) и собственный buffer/query-кэш базы. Каждый из них — граница корректности, а не просто трюк ради скорости. Сеньорский ход — назначить владение до любого тюнинга: решить, какой слой является источником истины для каждого вида данных, и позволить остальным держать копии только по правилам, уважающим этот источник.

Чистое разделение, к которому сходится большинство команд:

  • Край CDN владеет общими, публичными, кэшируемыми-по-URL ответами: статические ассеты, анонимный HTML, публичные API-чтения. Он не должен держать ничего пользовательского.
  • Reverse proxy владеет защитой origin: схлопыванием дублирующихся запросов (request coalescing), чтобы холодный кэш не устроил dogpile на приложение, плюс короткий кэш горячих публичных ответов близко к origin.
  • Redis владеет вычисленным состоянием приложения: дорогим результатом запроса, отрендеренным фрагментом, сессией — с ключом, которым управляет приложение, чтобы оно могло инвалидировать точечно.
  • БД владеет истиной. Её query/buffer-кэш ускоряет чтения, но инвалидируется записями автоматически; его редко тюнят, его уважают.

Ошибись во владении — и никакой TTL не спасёт. Падение Steam было нарушением владения: персонализированный ответ (работа Redis/origin) оказался во владении общего CDN-кэша, у которого не было способа узнать, что ответ персонализирован.

TTL должны каскадировать вниз, а не вверх

Самый частый баг композиции: внешний слой держит TTL длиннее, чем внутренний. Если у CDN s-maxage=3600, но приложение реревалидирует свой Redis-фрагмент каждые 60 секунд, то до часа CDN отдаёт версию, которую приложение уже считает мёртвой. Инвалидация на внутреннем слое невидима внешнему — край продолжает отгружать труп.

Правило: окна свежести должны сжиматься по мере удаления от origin — иначе ты обязан явно purge-ить внешний слой на каждое внутреннее изменение. Cache-Control: public, s-maxage=60, stale-while-revalidate=600 читается общим кэшем (CDN) через s-maxage; max-age нацелен на приватный кэш браузера; это намеренно разные числа для разных слоёв. Перепутать их — выдать браузеру год, а CDN минуту, когда имелось в виду наоборот, — это как деплой выкатывается везде, кроме того единственного кэша, в который пользователи реально попадают.

РесурсCDN (общий)Браузер (приватный)Инвалидация
Хешированный JS/CSS-ассетpublic, max-age=31536000, immutableто же — 1 годНовый хеш = новый URL. Никогда не purge.
Анонимный маркетинговый HTMLs-maxage=300, stale-while-revalidate=86400max-age=0, must-revalidateTag-based purge при публикации.
HTML дашборда за логиномprivate, no-store (CDN обязан пропустить)private, no-cacheНикогда не попадает в общий кэш.
Публичное API-чтение (прайс-лист)s-maxage=30, stale-if-error=3600no-cacheSurrogate-key purge при смене цены.

private — это шов, где утекают данные

Урок Steam в том, что общие кэши по умолчанию кэшируют выглядящие-кэшируемыми GET-ответы, а 200 без Cache-Control выглядит кэшируемым. Защита для персонализированного контента — один токен: private говорит общим кэшам «это для одного пользователя, не храни меня», при этом всё ещё разрешая браузеру кэшировать ответ. Для по-настоящему чувствительных ответов эскалируешь до no-store (никто не кэширует, нигде). Опасная середина — страница за логином, которая опускает директиву и верит, что приложение отдаёт её прямо клиенту: верно, пока в пути не окажется CDN, прокси или неверно настроенное правило surrogate-key. Разработчики редко харденят GET-эндпоинты под промежуточное кэширование, потому что предполагают доставку напрямую клиенту; именно это предположение изменение конфига на краю может отозвать, не предупредив их.

Почему это работает

Почему не закэшировать всё и добавить Vary: Cookie? Потому что Vary: Cookie включает в ключ кэша всю куку, поэтому каждая отдельная сессия — отдельная запись кэша: hit rate валится к нулю, а ты платишь за CDN ради приватного кэша. Хуже того, одна пропущенная нормализация куки — и два пользователя делят ключ. Для пользовательского контента правильный ответ почти всегда private/no-store, а не хитрый Vary.

Инвалидация — это задача распространения, а не событие

Когда меняется источник истины, изменение обязано пройти наружу через каждый слой, держащий копию, — и каждый слой инвалидируется по-своему. Сброс Redis не делает ничего с CDN; purge CDN не делает ничего с reverse proxy, всё ещё держащим устаревший объект. Три механизма, в порядке предпочтения:

  1. Версионированные/immutable URL для статических ассетов: новый content hash — это новый URL, поэтому инвалидировать нечего — старое и новое сосуществуют, а старое просто стареет. Поэтому хешированные бандлы отдаются с immutable, max-age=31536000.
  2. Tag / surrogate-key purge для динамики: помечай каждый ответ ключами (product:42, category:shoes), затем один вызов purge сбрасывает каждый закэшированный ответ с этим тегом по всему краю, независимо от URL. Instant purge у Cloudflare распространяется глобально меньше чем за 150 мс; tag-based и широкие purge обычно доходят до каждой edge-ноды за секунды — несколько минут.
  3. TTL + stale-while-revalidate как пол eventual consistency: даже без purge данные самоизлечиваются в пределах своего окна свежести.

Сеньорский дизайн вшивает purge в путь записи: мутация, меняющая истину, также ставит в очередь purge для каждого внешнего слоя, по порядку. Пропусти слой — и ты построил систему, корректную в базе и неверную на краю: тяжелейший класс багов для воспроизведения, потому что он проявляется только на закэшированном запросе.

Fail open: stale лучше падения

Композиция обязана пережить исчезновение origin. stale-if-error позволяет каждому общему слою продолжать отдавать последнюю хорошую копию, когда origin возвращает 5xx или недоступен — s-maxage=60, stale-if-error=86400 означает «свежо минуту, но вместо 503 пользователю отдай контент возрастом до суток, пока origin лежит». Это превращает падение origin в мягкую деградацию вместо стены ошибок. Трейдофф, который взвешивает сеньор: насколько stale приемлемо для каждого ресурса. Прайс-лист, отданный с часовым устареванием при падении, — нормально; флаг наличия товара, отданный stale, может привести к перепродаже. Ставь stale-if-error длинным для того, что терпит устаревание, и коротким или нулевым для того, что должно быть корректным или отсутствовать.

Выбери лучший вариант

Страница дашборда за логином рендерит пользовательские данные и стоит за CDN. Выбери политику кэширования для этого HTML-ответа.

Викторина

У CDN s-maxage=3600 на странице, но приложение реревалидирует свой Redis-фрагмент для этой страницы каждые 60 секунд. После правки контента что увидит пользователь?

Викторина

Ты хочешь, чтобы хешированный бандл (app.a1b2c3.js) кэшировался максимально агрессивно. Какая политика правильная?

Расставь шаги по порядку

Товар отредактировали. Расставь, как корректная инвалидация распространяется наружу через стек:

  1. 1 Запись коммитится в БД — источник истины меняется; её query-кэш инвалидируется автоматически
  2. 2 Путь записи удаляет/обновляет затронутые ключи Redis (вычисленный фрагмент, закэшированный запрос)
  3. 3 Тот же путь purge-ит устаревший объект reverse proxy для этого ресурса
  4. 4 Он отправляет tag/surrogate-key purge на CDN (product:42), сбрасывая каждую edge-копию
  5. 5 Браузеры самоизлечиваются на следующей реревалидации; stale-while-revalidate закрывает разрыв
Вспомните перед уходом
  1. 01
    Страница корректна в базе и корректна в Redis, но пользователи всё ещё видят stale-контент. Пройди, где баг и как слои должны были быть связаны.
  2. 02
    Почему отсутствие директивы `private` на странице за логином — это баг безопасности, а не просто вопрос hit rate, и каков правильный спектр политик?
Итог

Стек кэшей корректен, только когда его слои компонуются. Начни с назначения владения: CDN держит общие публичные ответы, reverse proxy защищает origin и схлопывает запросы, Redis держит вычисленное состояние приложения под управляемыми приложением ключами, а БД владеет истиной. TTL должны сжиматься по мере движения наружу — иначе каждая внутренняя инвалидация требует явного внешнего purge, иначе край продолжает отдавать контент, который приложение уже убило: баг, проявляющийся только на закэшированном запросе. Ограничивай пользовательские ответы через private (или no-store, когда чувствительно), потому что общие кэши хранят неаннотированный 200 по умолчанию — это шов, где утекают аутентифицированные данные, как показал инцидент Steam 2015. Вшей инвалидацию в путь записи, чтобы мутация распространялась наружу через Redis, прокси и tag-based purge на CDN по порядку; предпочитай версионированные/immutable URL, чтобы статика вообще не нуждалась в purge. Наконец, падай мягко: stale-if-error позволяет каждому слою отдавать последнюю хорошую копию сквозь падение origin, превращая стены 5xx в мягкую деградацию — длинным для ресурсов, терпящих устаревание, и коротким для тех, что должны быть корректными или отсутствовать.

Продолжить восхождение ↑Кэширование, капстоун: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.