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-кеша на браузъра и кеша в паметта на приложението. Колкото по-популярен е записът — толкова по-голяма е потенциалната щета от 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 или дори мютекс в паметта.
Ключов параметър — lock_timeout. Ако е твърде кратък, първата нишка може да не успее да обнови кеша и втората нишка също ще се опита да зареди данни, създавайки мини-stampede. Ако е твърде дълъг — клиентите чакат по-дълго от необходимото. Препоръчителна стойност: 2–5 секунди за типични заявки към база данни.
Проблем с една нишка — при отказ на първата нишка (изключение, timeout) блокировката остава заета и всички останали нишки чакат до изтичане на 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. Атомността гарантира, че две едновременни заявки не могат да получат блокировка — само една, което напълно предотвратява 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 за всеки популярен ключ.
Да, ако множество нишки в приложението използват общ кеш в паметта. Например 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също