Кеширование
Dogpile: построй и укроти стадо
Читать про dogpile — не то же, что вытаскивать сервис из него. Подними небольшой сервис с одним дорогим горячим ключом, загони реальное стадо в origin в момент истечения, затем реализуй single-flight, leased distributed lock и XFetch — измеряя разлёт по origin на каждом шаге.
Преврати ментальную модель юнита в воспроизводимый цикл: воспроизведи стадо, затем добавь коалесинг, distributed-блокировку с продлеваемым lease и зафенсенной записью, и раннее вероятностное истечение — доказывая счётчиками на стороне origin, что каждый схлопывает N одновременных пересчётов к одному.
Построй небольшой cache-aside-сервис с одним дорогим горячим ключом, воспроизведи измеримый dogpile в момент истечения под конкурентной нагрузкой, затем реализуй и измерь три митигации — single-flight, leased distributed lock и XFetch — доказав, что каждая схлопывает число пересчётов origin числами до/после.
- Таблица до/после по всем четырём стратегиям: число пересчётов origin на истечение, p99-латентность и максимальное устаревание — измеренные под одинаковой конкурентной нагрузкой, а не оценённые.
- Вариант с distributed-блокировкой показывает ~1 пересчёт origin на истечение по всему флоту, а тесты с инъекцией сбоя доказывают, что (а) убитый держатель авто-освобождается по TTL без дедлока и (б) держатель, приостановленный дольше lease, не может перезаписать более новое значение (зафенсенная запись отвергнута).
- Вариант XFetch показывает, что горячий ключ пересчитывается рано и в одиночку до того, как его TTL дойдёт до нуля, удерживая число пересчётов origin около 1 без удержания блокировки.
- Абзац-вывод, выбирающий стратегию по умолчанию для этой нагрузки и обосновывающий её против толерантности к stale, числа инстансов и стоимости пересчёта — и называющий сбой, от которого защищает каждая мера (TTL блокировки, продление lease, fencing-токен).
- Добавь on-call-ранбук: как заметить dogpile в метриках (всплески пересчётов origin, синхронные с границами TTL), дерево решений single-flight против distributed lock против XFetch, и аварийный выход из дедлока блокировки (всегда имей TTL).
- Добавь джиттер TTL ПАЧКЕ холодных ключей и покажи, что он разносит их совместное истечение — затем покажи, что для одного горячего ключа он ничего не делает, делая область применимости джиттера конкретной.
- Добавь stale-while-revalidate (разделение soft-TTL / hard-TTL), чтобы ждущие никогда не блокировались: отдавай stale-значение мгновенно и обновляй в фоне, и сравни его профиль устаревания/латентности с блокирующей блокировкой.
- Прогони вариант с distributed-блокировкой против failover Redis (или Redlock по нодам) и задокументируй, что происходит с гарантией exactly-one во время partition — связывая обратно с тем, зачем нужны fencing-токены.
Это цикл, который ты прогонишь, когда горячий ключ кладёт origin: воспроизведи стадо в момент истечения, измерь разлёт по origin, затем схлопни его. Локальный single-flight капит пересчёты числом инстансов; distributed lock схлопывает флот до одного — но только с TTL длиннее худшего пересчёта, продлеваемым lease, условным освобождением и зафенсенной записью, чтобы пережить упавшего или приостановленного держателя. XFetch растворяет коллизию без блокировки, пересчитывая рано и в одиночку. Сделав это раз на игрушечном сервисе, со счётчиками origin и инъекцией сбоев, ты доводишь production-версию до мышечной памяти.