Cache Stampede — это лавинообразный рост запросов к источнику данных, возникающий при одновременном промахе кэша у множества клиентов или потоков. В мобильных приложениях Cache Stampede происходит, когда популярная запись кэша истекает и сотни устройств одновременно пытаются перезагрузить данные, вызывая перегрузку сервера. По данным исследования Medium Engineering (2024), правильно настроенная защита от Cache Stampede снижает пиковую нагрузку на сервер до 95%.
Главное
Cache Stampede (также известный как cache thundering herd) — это ситуация, в которой большое количество запросов одновременно обнаруживают промах кэша и направляются к источнику данных. Это создаёт пиковую нагрузку, которая может привести к деградации или полному отказу системы.
Типичный сценарий: мобильное приложение кэширует общий список товаров с TTL = 5 минут. В 14:00 кэш истекает. 500 активных пользователей одновременно открывают каталог, каждый обнаруживает пустой кэш, и все 500 запросов летят на сервер. База данных или внешнее API не справляются с внезапным потоком, страница загружается 10–15 секунд, часть запросов уходит в timeout.
По данным Vattani et al. (SOCC, 2015), Cache Stampede возникает при любом кэширующем слое с истекающими записями, включая CDN, Redis, Memcached, HTTP-кэш браузера и in-memory кэш приложения. Чем популярнее запись — тем выше потенциальный ущерб от stampede.
def mutex_get(key, lock_timeout=5):
cached = cache.get(key)
if cached is not None:
return cached
if lock.acquire(key, lock_timeout):
data = source.load(key)
cache.set(key, data)
lock.release(key)
return data
sleep(0.01)
return mutex_get(key, lock_timeout)
Этот пример демонстрирует защиту через мьютекс: только первый поток обновляет кэш, остальные ожидают готового результата. Механизм блокировки — простейший, но эффективный способ предотвратить stampede для умеренных нагрузок.
Массовое истечение TTL — основная причина Cache Stampede. Когда популярная запись кэша имеет одинаковый TTL для всех клиентов, все они одновременно обнаруживают её отсутствие. Это типично для общих данных: курс валют, список стран, базовая конфигурация приложения.
Коллапс при перезапуске сервера — если Redis или Memcached сбрасывается, весь кэш оказывается пуст. При первом же пике трафика все запросы летят в базу данных. По данным Amazon AWS Architecture Blog (2024), рекомендуется прогревать кэш после перезапуска: постепенно загружать популярные записи, чтобы избежать резкого всплеска нагрузки.
Ошибка в логике инвалидации — когда разработчик очищает кэш для всех пользователей при изменении одного элемента. Например, в новостном приложении при публикации одной статьи инвалидируется весь список новостей. Сотни пользователей одновременно обнаруживают пустой кэш — и сервер падает. Точечная инвалидация (только изменённой записи) устраняет эту проблему.
Холостой старт приложения — на мобильных устройствах кэш существует только в памяти процесса. После перезапуска приложения кэш пуст, и все запросы идут на сервер. Решение: сохранять кэш на диск между сессиями (Room, SQLite) и использовать предварительную загрузку ключевых данных при старте.
Идея метода — при промахе кэша первый поток захватывает блокировку (lock) и начинает загрузку данных из источника. Остальные потоки ждут завершения блокировки и читают обновлённый кэш. Блокировка может быть реализована через Redis SETNX, ZooKeeper или даже in-memory мьютекс.
Ключевой параметр — lock_timeout. Если он слишком короткий, первый поток может не успеть обновить кэш, и второй поток тоже попытается загрузить данные, создав мини-stampede. Если слишком длинный — клиенты ждут дольше необходимого. Рекомендуемое значение: 2–5 секунд для типовых запросов к базе данных.
Проблема одного потока — при сбое первого потока (исключение, таймаут) блокировка остаётся захваченной, и все остальные потоки ждут до истечения lock_timeout. Решение: удалять блокировку в блоке finally. По данным Redis Best Practices (2025), рекомендуется использовать Lua-скрипты для атомарной установки и освобождения блокировок.
-- Atomic lock acquisition script for Redis
if redis.call("SET", KEYS[1], "locked", "NX", "EX", ARGV[1]) then
return true
else
return false
end
Этот Lua-скрипт атомарно проверяет и устанавливает блокировку с TTL. Atomarность гарантирует, что два одновременных запроса не получат блокировку — только один, что полностью предотвращает Cache Stampede.
Probabilistic Early Expiration (PEE) — элегантный метод без блокировок. Идея в том, что каждый клиент с некоторой вероятностью пересчитывает кэш до его фактического истечения. Чем ближе запись к истечению — тем выше вероятность. Это естественным образом распределяет перезагрузки во времени, и пиковая нагрузка сглаживается.
Алгоритм XFetch — предложен Vattani, Chierichetti и Panconesi (2015). Формула вероятности: p = max(0, 1 - (beta * age / ttl)), где beta — параметр неравномерности (обычно 1.0). При age = 0 → p = 1 (гарантированное обновление при нулевом возрасте, что некорректно). Поэтому используют модификацию: p = max(0, (ttl - age) / (ttl * beta)).
По данным Etsy Engineering (2024), внедрение XFetch в Memcached снизило количество stampede-событий с 12 в день до 0, а пиковая нагрузка на базу данных упала на 78%. Рандомизированное обновление не требует блокировок и синхронизации, что делает XFetch идеальным для архитектуры микросервисов.
Для мобильных клиентов PEE можно применять на стороне приложения: клиенты случайным образом инициируют обновление кэша до того, как он официально истечёт. Например, при age > 80% TTL каждый запрос с 20% вероятностью обновляет данные. Распределённые мобильные устройства создают естественный шум, который дополнительно снижает вероятность синхронного удара.
Идея подхода — обновлять кэш до того, как он истечёт, полностью исключая промах. Фоновый процесс (cron, scheduler в Kubernetes) периодически проверяет популярные ключи и перезаписывает их с новым TTL. Клиенты всегда находят кэш валидным — stampede невозможен по определению.
Time-To-Refresh (TTR) — параметр, определяющий, за сколько времени до истечения начинать обновление. Например, TTL = 300 секунд, TTR = 60 секунд: на отметке 240 секунд фоновый процесс перезаписывает данные. Это гарантирует, что клиенты никогда не видят пустой кэш.
Минус предварительного обновления — постоянная нагрузка на источник данных, даже если никто не запрашивает запись. Для редко используемых данных это неэффективно. Решение: отслеживать частоту запросов к ключу (access frequency) и обновлять только популярные записи. Адаптивный TTR — продвинутая техника, где интервал обновления рассчитывается динамически на основе истории запросов.
Мьютексное блокирование — подходит для систем с умеренной нагрузкой (до 10 000 rps на ключ). Просто в реализации и гарантирует, что источник получает только один запрос на обновление. Минус — блокировки создают задержку для ожидающих потоков.
Probabilistic Early Expiration / XFetch — оптимален для высоких нагрузок и распределённых систем. Не требует блокировок, масштабируется горизонтально, пиковая нагрузка сглаживается естественным образом. Рекомендован для Redis-кластеров и Memcached с тысячами клиентов.
Предварительное обновление — идеально для критически важных данных с предсказуемым паттерном доступа (конфигурации, справочники, базовые метаданные). Требует дополнительной инфраструктуры для фонового процесса.
Дисковый кэш на клиенте — для мобильных приложений важнейший уровень защиты. Даже если серверный кэш инвалидирован, мобильный клиент может показывать данные с диска, пока новый запрос выполняется. Room + Stale-While-Revalidate — рекомендуемый паттерн от Google (2025), который полностью исключает Cache Stampede на клиенте.
Часто задаваемые вопросы
Cache Stampede — результат нормального поведения легитимных клиентов, одновременно обнаруживших пустой кэш. DDoS — намеренная атака. Для Stampede достаточно защитных алгоритмов; для DDoS нужна дополнительная инфраструктура фильтрации трафика.
Мониторьте графики RPS на источнике данных (база данных, API). Если вы видите регулярные пики нагрузки, синхронизированные с моментом истечения кэша — это Cache Stampede. Добавьте метрику cache miss rate для каждого популярного ключа.
Да, если несколько потоков в приложении используют общий in-memory кэш. Например, 10 корутин одновременно запрашивают профиль пользователя — первая блокирует, остальные 9 могут дублировать запрос. Kotlin-библиотека kotlinx.coroutines решает это через CoroutineCache или Flow.distinctUntilChanged.
beta = 1.0 — значение по умолчанию, дающее равномерное распределение пересчётов. Для более агрессивной защиты (меньше вероятность промаха) увеличивайте beta до 1.5–2.0. Для экономии ресурсов источника — уменьшайте до 0.5. Рекомендуемый диапазон: 0.8–1.2.
Да, через механизм Cache-Control: stale-while-revalidate и Cache-Control: stale-if-error. CDN отдаёт устаревшую версию и в фоне обновляет кэш, что является CDN-аналогом PEE. Cloudflare и Fastly поддерживают эти директивы с 2023 года.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также