open atlas
← Все проекты

backend · intermediate · 4d

Лаборатория cache stampede

Воспроизведи thundering-herd промах кэша под нагрузкой, затем убей его через single-flight и пересчёт с ранним истечением.

Read-through кэш выглядит как выигрыш в латентности, пока горячий ключ не истекает и источник не получает тысячу одновременных промахов — кэш только что усилил одно событие истечения в thundering herd. Этот проект заставляет воспроизвести этот режим отказа под измеримой нагрузкой, увидеть скачок origin QPS на графике, а затем убить его двумя дополняющими оружиями: коалесцированием через single-flight (только один горутин/промис гонится к источнику, остальные ждут его результата) и вероятностным ранним истечением (XFetch пересчитывает горячий ключ до реального истечения, и обрыв жёсткого TTL исчезает). Сеньорный инсайт: TTL — не просто бюджет свежести, это синхронизированный таймер, разделённый всеми активными запросами, и правильное сочетание jitter, раннего пересчёта и схлопывания запросов превращает этот обрыв в плавный склон.

Результат

Демо, где истечение горячего ключа шлёт 1 запрос к источнику вместо тысяч, показанное на графике задержек.

Этапы

0/2 · 0%
  1. 01Спровоцируй стампид

    Собери read-through кэш и под нагрузкой вызови stampede при истечении.

    Критерии готовности
    • Под нагрузкой истечение одного горячего ключа шлёт всплеск параллельных запросов в ориджин — виден как скачок origin-QPS на графике.
    • Кэш read-through: промах пересчитывает и заново заполняет ключ.
  2. 02Схлопни промахи в один вызов ориджина

    Добавь single-flight, чтобы конкурентные промахи по одному ключу схлопывались в один вызов источника.

    Критерии готовности
    • Параллельные промахи по одному ключу дают ровно один вызов ориджина; остальные ждут и переиспользуют его результат.
    • Раннее/вероятностное истечение пересчитывает до жёсткого истечения, так что горячий ключ под нагрузкой почти не истекает полностью.

Стартер

  • README.md
  • src/cache.ts
  • test/cache.test.ts
Скачать стартер (.zip)

Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test

Рубрика

Джуниор Миддл Сеньор
Воспроизведение стампида Кэш промахивается при истечении, но нагрузочный тест не инструментирован; origin QPS во время стампида не измеряется. Нагрузочный тест истекает один горячий ключ под устойчивой конкурентной нагрузкой и показывает скачок origin QPS (например, 1 запрос → N параллельных промахов) на графике с зафиксированным числом промахов. Ты воспроизводишь стампид, измеряешь точный fan-out (соотношение параллельных промахов к вызовам источника) и объясняешь, почему jitter TTL сам по себе не чинит горячий ключ — он лишь снижает вероятность столкновения, когда несколько ключей истекают в одном окне.
Коалесцирование single-flight Все параллельные промахи независимо вызывают источник; дедупликации активных запросов нет. Single-flight (или мьютекс на ключ) гарантирует, что параллельные промахи по одному ключу схлопываются в один вызов источника; остальные ждут и переиспользуют результат. Ты знаешь патологический случай: если вызов источника падает, single-flight транслирует ошибку всем ожидающим — одна ошибка источника становится N ошибками запросов. Ты решаешь это независимыми повторами при неудаче, а не кешированием ошибки, и измеряешь стоимость латентности очереди ожидания при высокой конкуренции.
Раннее истечение и выбор TTL TTL — фиксированная константа, выбранная интуитивно; кэш всегда промахивается на жёстком истечении, никогда не пересчитывает заблаговременно. Stale-while-revalidate или вероятностное раннее истечение XFetch пересчитывает горячий ключ до срабатывания жёсткого TTL, и обрыв на границах истечения исчезает под нагрузкой. Ты настраиваешь параметр beta XFetch под измеренное время пересчёта источника: слишком малый — и пересчёт не начинается достаточно рано; слишком большой — и пересчёт происходит почти на каждом запросе. Ты представляешь кривую «устаревание против нагрузки на источник» и защищаешь выбранный дефолт числами из нагрузочного теста.
Эталонный разбор (спойлер)

Почему возникает thundering herd: популярный ключ истекает, и все активные запросы одновременно наблюдают промах. Сама атомарность кэша порождает проблему: без координации N читателей каждый решает пересчитать, N вызовов источника стреляют, и кэш только что умножил единственное истечение в fan-out, пропорциональный конкуренции запросов.

Single-flight как основной фикс: схлопни все параллельные промахи по одному ключу в один вызов источника и разошли результат. Компромисс — латентность: ожидающие блокируются на время пересчёта. Ловушка распространения ошибок (единственная ошибка источника расходится всем ожидающим) означает: не кешируй ошибки — позволь неудачам повторяться независимо.

Вероятностное раннее истечение XFetch: пересчитывай ключ с вероятностью, пропорциональной оставшемуся TTL и стоимости пересчёта, чтобы пересчёт распределялся по окну TTL, а не концентрировался на границе. Формула: пересчитывай, если `now - delta * beta * log(rand()) > expiry_time`. Beta ≈ 1 — хорошая отправная точка; увеличивай, если вызовы источника дороги.

Только jitter TTL недостаточно: jitter разносит истечения во времени, чтобы несколько ключей не истекали в один миг, снижая межключевые столкновения, но единственный очень горячий ключ всё равно устраивает стампид при собственном истечении независимо от jitter. Используй jitter для диверсификации ключей, single-flight или раннее пересчёт — для защиты горячих ключей.

Сделай по-сеньорски

  • Добавь вероятностное раннее истечение (XFetch) и покажи, что оно убирает обрыв на границах TTL.
  • Измерь компромисс между устареванием и нагрузкой на источник и выбери дефолты, которые сможешь обосновать.

Навыки

TTL designsingle-flight / request coalescingprobabilistic early expirationload testing