backend · intermediate · 4d
Лаборатория cache stampede
Воспроизведи thundering-herd промах кэша под нагрузкой, затем убей его через single-flight и пересчёт с ранним истечением.
Результат
Демо, где истечение горячего ключа шлёт 1 запрос к источнику вместо тысяч, показанное на графике задержек.
Этапы
0/2 · 0%- 01Спровоцируй стампид
Собери read-through кэш и под нагрузкой вызови stampede при истечении.
Критерии готовности- Под нагрузкой истечение одного горячего ключа шлёт всплеск параллельных запросов в ориджин — виден как скачок origin-QPS на графике.
- Кэш read-through: промах пересчитывает и заново заполняет ключ.
- 02Схлопни промахи в один вызов ориджина
Добавь single-flight, чтобы конкурентные промахи по одному ключу схлопывались в один вызов источника.
Критерии готовности- Параллельные промахи по одному ключу дают ровно один вызов ориджина; остальные ждут и переиспользуют его результат.
- Раннее/вероятностное истечение пересчитывает до жёсткого истечения, так что горячий ключ под нагрузкой почти не истекает полностью.
Стартер
- README.md
- src/cache.ts
- test/cache.test.ts
Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: 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.
- Измерь компромисс между устареванием и нагрузкой на источник и выбери дефолты, которые сможешь обосновать.