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

Кеширование

Метастабильный сбой, fencing-токены и production-постмортемы

Суть Как немитигированный stampede толкает систему в self-sustaining retry-шторм, из которого она не выходит сама; fencing-токен фикс для lock TTL race; четыре production-постмортема; упражнение по дизайну трёхтирового глобального кеша.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 18 min

Stampede длится 10 секунд. Через четыре часа БД всё ещё на 100% CPU. Stampede закончился — но система не может само-восстановиться. Это паттерн метастабильного сбоя: как только система падает в retry-шторм, она там и остаётся.

Последовательность метастабильного сбоя

Cache stampede может эскалировать в self-sustaining сбой:

  1. T=0 — TTL срабатывает, стадо бьёт в БД. CPU БД насыщается.
  2. T=0–5 с — запросы в очереди. Глубина очереди превышает client request timeout (например, 5 с).
  3. T=5 с — начинаются client timeouts. Большинство client SDK авто-ретраят с exponential backoff. Backoff короткий (100 мс до 1 с), потому что исходный timeout выглядел как transient network failure, а не перегруженный сервер.
  4. T=5–30 с — ретраи умножают стадо. Исходные 10 000 misses становятся 30 000 in-flight запросами (3 попытки на клиента). БД теперь обслуживает ретраи вместо свежих запросов. Rebuild кеша не может завершиться, потому что БД слишком занята ответами на rebuild-запросы.
  5. T=4 ч — система остаётся насыщенной. Каждый новый приход ретраит. Ретраи предотвращают восстановление БД. БД предотвращает rebuild кеша. Пустой кеш генерирует больше ретраев. Self-sustaining loop.

Система имеет два стабильных состояния: healthy (кеш полный, низкая нагрузка БД) и storm (кеш пустой, БД на 100%). Возмущение переместило её из healthy в storm. Она не может вернуться сама.

Почему система не может само-восстановиться

Loop: нет кеша → ретраи → перегрузка БД → нет DB capacity → нет rebuild кеша → нет кеша. Каждый компонент корректно следует своему проектному поведению. Ни один компонент не «знает», что он в шторме. Client SDK ретраит, потому что видит таймауты. БД обрабатывает запросы по порядку. Кеш не rebuild-ится, потому что БД никогда не отвечает достаточно быстро.

Восстановление требует ломки loop извне:

  1. Load-shedding на gateway — возвращать 503 Service Unavailable немедленно, когда CPU БД превышает 90%. Большинство client SDK трактуют 503 как «не ретраить немедленно» и отступают с длинным jitter. Через 30–60 с очередь дренируется, БД восстанавливается и кеш rebuild-ится.
  2. Circuit-breaker на rebuild-пути — если rebuild-запрос падает или занимает слишком долго, отдавать stale forever до восстановления БД. Ломает зависимость между rebuild кеша и здоровьем БД.
  3. Manual cache warmup — операторы напрямую пишут известные-хорошие значения в кеш, обходя rebuild-путь. Немедленно возвращает систему в healthy состояние.
Механизм восстановленияКак ломает loopПобочный эффект
503 на gatewayКлиенты перестают ретраить503-ы видны пользователям 30–60 с
Circuit-breakerRebuild перестаёт зависеть от БДStale данные отдаются бесконечно
Manual warmupВставляет значения кеша напрямуюТребует действий оператора

Fencing-токен фикс для lock TTL race

Redis лок с SET lock:key uuid EX 30 NX имеет race condition когда rebuild переживает EX:

  1. Запрос-1 захватывает лок с uuid-A, EX=10 с. Запускает 12-секундный rebuild.
  2. T=10 с: лок авто-истекает. Запрос-2 захватывает лок с uuid-B.
  3. T=12 с: запрос-1 завершает rebuild. Пишет в кеш.
  4. T=12 с: запрос-2 тоже завершает rebuild. Пишет в кеш.
  5. Результат: дублирование записей. Если есть параллельная инвалидация из DB-записи, stale значение запроса-1 может перезаписать более новое корректное значение.

Фикс 1: увеличить EX выше rebuild p99.9. Большинство дублей предотвращено. Не защищает от всех race.

Фикс 2: проверка fencing-токена перед записью.

# Перед записью rebuild-нутого значения:
current_lock = GET lock:key
if current_lock != my_uuid:
  ABORT  # мы потеряли лок; кто-то другой делает rebuild
ELSE:
  SET cache:key new_value EX 60  # писать только если ещё держим лок

Фикс 3: монотонная версия на ключ. Включать версию в каждую запись кеша. БД или кеш-слой отвергает записи с version ≤ текущей версии. Stale rebuilds пишут со старой версией и тихо отвергаются. Критика Мартина Клеппманна 2016 года RedLock формализовала это: distributed locks одних недостаточно для корректности; нужен fencing.

XFetch: почему экспонента оптимальна

Vattani et al. (VLDB 2015) доказывают, что ни один coordination-free алгоритм не может сделать лучше экспоненты для удержания ожидаемых refresh на TTL-окно ровно в 1. Любое более жёсткое правило либо:

  • Требует координации (distributed lock, lease) для ограничения variance, либо
  • Принимает большую variance (некоторые окна получают 0 refresh → stampede, некоторые много → потраченная работа).

Экспонента уникально оптимальна, потому что: минимум N независимых Exp(λ) выборок есть Exp(N·λ). С ростом размера флота N «выигрывающий» читатель срабатывает раньше — но ожидаемый total остаётся 1. Никакое другое распределение не имеет этого свойства масштабирования, оставаясь безмятежным (без состояния на читателя).

Четыре production-постмортема

Reddit 2017. Изменение feed-кеша задеплоено без single-flight. Результат: 3× DB пики на каждой TTL-границе. On-call пейджнули по CPU БД через 30 минут после деплоя; откатили за 1 час.

Shopify 2020 (Black Friday). Flash-sale главная с TTL=30 с без SWR. На 12:00:30 TTL сработал под 10× нормального трафика. Весь storefront edge имел multi-second outages на каждой TTL-границе первые 10 минут продажи. Починили деплоем stale-grace=5 мин на следующем пуше.

Twitter 2022. Unrelated баг инвалидации кеша триггерил 30М параллельных rebuild-попыток. DB connection pool насытился на 4 часа. Восстановление потребовало load-shedding + manual cache seed.

Cloudflare 2024. Баг coalescing в KV кеше кратко позволял N запросам на один ключ обходить coalescing в окне деплоя. Origin видел пропорциональную нагрузку в окне. Починили деплоем guard flag, отключившего rollout coalescing до патча.

Паттерн всех четырёх: stampede-защита либо отсутствовала, либо была мис-конфигурена, либо имела баг, побеждающий её под нагрузкой. Цена была пропорциональна gap-у защиты.

Дизайн full-stack композиции

Production кеш-стек для глобального высокотрафикового сайта нуждается в защите на каждом тире:

ТирМеханизмСнижение stampede
CDN edgeSWR + request coalescingN параллельных misses → 1 origin-запрос
Application кеш (Redis)XFetch + single-flight + lockN параллельных misses → 1 rebuild за TTL-окно
БДCircuit-breaker, read replicasOrigin load shedding при перегрузке

Каждый тир должен независимо ограничивать своё стадо — сбой на любом тире каскадирует на следующий. SWR на CDN поглощает 99% TTL-boundary трафика. XFetch предотвращает stampede следующего слоя, обновляясь до истечения. Single-flight схлопывает per-node стада. Redis lock схлопывает cross-node для 1% горячих ключей. TTL jitter десинхронизирует multi-key границы. Вместе они снижают 10 000 параллельных misses до 1 rebuild в любом месте стека.

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

Bouman et al. (SOSP 2024) формализовали паттерн метастабильного сбоя как класс: система в метастабильном сбое стабильна и в healthy, и в storm состоянии, и возмущение перемещает её между ними. Переходы: healthy → storm (stampede + retry-амплификация) и storm → healthy (внешнее вмешательство). Их ключевой результат: любая система с at-least-once ретраями и без load-shedding может быть продвинута в метастабильность достаточно большой транзиентной перегрузкой. Фикс — явный «kill the herd» механизм (503-on-overload, circuit-breaker), отдельный от логики кеширования.

Викторина

Система входит в метастабильный сбой после stampede. CPU БД держится на 100% 4 часа. Кеш пустой. Какой правильный диагноз?

Викторина

Redis лок использует EX=10 с, но rebuild-ы могут занимать до 15 с. Rebuild стартует в T=0 с. Что именно происходит в T=10 с?

Викторина

Глобальный новостной сайт нуждается в stampede-защите на каждом кеш-тире. Какой стек удовлетворяет: p99 латентность под 200 мс при вирусных спайках 10× И БД не видит более 5× steady-state QPS?

Вспомните перед уходом
  1. 01
    Объясни метастабильную форму сбоя, следующую за немитигированным cache stampede, и почему система не может само-восстановиться.
  2. 02
    Redis лок с EX=10 с производит дублирующие записи при длинных rebuild. Объясни трёхкомпонентный фикс.
  3. 03
    Почему свойство XFetch 'ожидаемый 1 refresh на TTL-окно' выполняется независимо от размера флота, а distributed lock требует явной cross-node координации?
Итог

Cache stampede, перегружающий БД, может эскалировать в метастабильный сбой длиной в часы: ретраи клиентов от тайм-аутнутых запросов поддерживают нагрузку, предотвращающую восстановление БД, которое предотвращает rebuild кеша, который генерирует больше ретраев. Ломка loop требует внешнего механизма — 503-on-overload на gateway, circuit-breaking rebuild-пути или ручного seed кеша. Distributed lock race (EX слишком короткий) требует трёх защит: extended EX, проверка fencing-токена перед записью и монотонная версия на ключах кеша. Полный production стек слоями: SWR на CDN edge, XFetch на application-кеше, single-flight per-process, Redis lock для горячих ключей — каждый тир снижает стадо на порядок. Четыре реальных инцидента (Reddit 2017, Shopify 2020, Twitter 2022, Cloudflare 2024) разделяют один паттерн: защита отсутствует, мис-конфигурена или побеждена под нагрузкой.

Связанные уроки
встречается в202
Продолжить восхождение ↑Cache stampede: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources6
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06

Trademarks belong to their respective owners. Editorial reference only.