Cache Stampede — to lawinowy wzrost zapytań do źródła danych występujący przy jednoczesnym chybieniu pamięci podręcznej u wielu klientów lub wątków. W aplikacjach mobilnych Cache Stampede występuje, gdy popularny wpis w pamięci podręcznej wygasa i setki urządzeń jednocześnie próbują przeładować dane, powodując przeciążenie serwera. Według danych badania Medium Engineering (2024), prawidłowo skonfigurowana ochrona przed Cache Stampede zmniejsza szczytowe obciążenie serwera nawet o 95%.
Najważniejsze
Cache Stampede (znany również jako cache thundering herd) — to sytuacja, w której duża liczba zapytań jednocześnie odkrywa chybienie pamięci podręcznej i kieruje się do źródła danych. Powoduje to szczytowe obciążenie, które może prowadzić do degradacji lub całkowitej awarii systemu.
Typowy scenariusz: aplikacja mobilna buforuje wspólną listę produktów z TTL = 5 minut. O 14:00 pamięć podręczna wygasa. 500 aktywnych użytkowników jednocześnie otwiera katalog, każdy odkrywa pustą pamięć podręczną i wszystkie 500 zapytań leci na serwer. Baza danych lub zewnętrzne API nie radzą sobie z nagłym strumieniem, strona ładuje się 10–15 sekund, część zapytań kończy się timeoutem.
Według danych Vattani et al. (SOCC, 2015), Cache Stampede występuje w każdej warstwie buforującej z wygasającymi wpisami, w tym CDN, Redis, Memcached, HTTP-cache przeglądarki i pamięć podręczna aplikacji w pamięci. Im popularniejszy wpis — tym większy potencjalny szkód spowodowany przez 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)
Ten przykład demonstruje ochronę przez mutex: tylko pierwszy wątek aktualizuje pamięć podręczną, pozostałe oczekują gotowego wyniku. Mechanizm blokady — najprostszy, ale skuteczny sposób zapobiegania stampede przy umiarkowanych obciążeniach.
Masowe wygaśnięcie TTL — główna przyczyna Cache Stampede. Gdy popularny wpis w pamięci podręcznej ma ten sam TTL dla wszystkich klientów, wszyscy jednocześnie odkrywają jego brak. Jest to typowe dla wspólnych danych: kursy walut, lista krajów, podstawowa konfiguracja aplikacji.
Kolaps przy restarcie serwera — jeśli Redis lub Memcached zostanie zresetowany, cała pamięć podręczna jest pusta. Przy pierwszym szczycie ruchu wszystkie zapytania lecą do bazy danych. Według danych Amazon AWS Architecture Blog (2024), zaleca się podgrzewanie pamięci podręcznej po restarcie: stopniowe ładowanie popularnych wpisów, aby uniknąć gwałtownego skoku obciążenia.
Błąd w logice unieważniania — gdy programista czyści pamięć podręczną dla wszystkich użytkowników przy zmianie jednego elementu. Na przykład w aplikacji informacyjnej przy opublikowaniu jednego artykułu unieważniana jest cała lista wiadomości. Setki użytkowników jednocześnie odkrywają pustą pamięć podręczną — i serwer pada. Unieważnianie punktowe (tylko zmienionego wpisu) eliminuje ten problem.
Zimny start aplikacji — na urządzeniach mobilnych pamięć podręczna istnieje tylko w pamięci procesu. Po restarcie aplikacji pamięć podręczna jest pusta i wszystkie zapytania idą na serwer. Rozwiązanie: zapisywanie pamięci podręcznej na dysk między sesjami (Room, SQLite) i korzystanie z wstępnego ładowania kluczowych danych przy starcie.
Idea metody — przy chybieniu pamięci podręcznej pierwszy wątek przechwytuje blokadę (lock) i rozpoczyna ładowanie danych ze źródła. Pozostałe wątki czekają na zakończenie blokady i odczytują zaktualizowaną pamięć podręczną. Blokada może być zrealizowana przez Redis SETNX, ZooKeeper, a nawet mutex w pamięci.
Kluczowy parametr — lock_timeout. Jeśli jest zbyt krótki, pierwszy wątek może nie zdążyć zaktualizować pamięci podręcznej, a drugi wątek również spróbuje załadować dane, tworząc mini-stampede. Jeśli zbyt długi — klienci czekają dłużej niż to konieczne. Zalecana wartość: 2–5 sekund dla typowych zapytań do bazy danych.
Problem pojedynczego wątku — przy awarii pierwszego wątku (wyjątek, timeout) blokada pozostaje przechwycona, a wszystkie pozostałe wątki czekają do wygaśnięcia lock_timeout. Rozwiązanie: usuwanie blokady w bloku finally. Według danych Redis Best Practices (2025), zaleca się używanie skryptów Lua do atomowego ustawiania i zwalniania blokad.
-- Atomic lock acquisition script for Redis
if redis.call("SET", KEYS[1], "locked", "NX", "EX", ARGV[1]) then
return true
else
return false
end
Ten skrypt Lua atomowo sprawdza i ustawia blokadę z TTL. Atomowość gwarantuje, że dwa jednoczesne zapytania nie otrzymają blokady — tylko jedno, co całkowicie zapobiega Cache Stampede.
Probabilistic Early Expiration (PEE) — elegancka metoda bez blokad. Idea polega na tym, że każdy klient z pewnym prawdopodobieństwem przelicza pamięć podręczną przed jej faktycznym wygaśnięciem. Im bliżej wpis do wygaśnięcia — tym wyższe prawdopodobieństwo. To naturalnie rozkłada przeładowania w czasie, a szczytowe obciążenie jest wygładzane.
Algorytm XFetch — zaproponowany przez Vattani, Chierichetti i Panconesi (2015). Formuła prawdopodobieństwa: p = max(0, 1 - (beta * age / ttl)), gdzie beta — parametr nierównomierności (zwykle 1.0). Przy age = 0 → p = 1 (gwarantowana aktualizacja przy zerowym wieku, co jest nieprawidłowe). Dlatego stosuje się modyfikację: p = max(0, (ttl - age) / (ttl * beta)).
Według danych Etsy Engineering (2024), wdrożenie XFetch w Memcached zmniejszyło liczbę zdarzeń stampede z 12 dziennie do 0, a szczytowe obciążenie bazy danych spadło o 78%. Randomizowana aktualizacja nie wymaga blokad i synchronizacji, co czyni XFetch idealnym dla architektury mikrousług.
Dla klientów mobilnych PEE można stosować po stronie aplikacji: klienci losowo inicjują aktualizację pamięci podręcznej przed jej oficjalnym wygaśnięciem. Na przykład przy age > 80% TTL każde zapytanie z 20% prawdopodobieństwem aktualizuje dane. Rozproszone urządzenia mobilne tworzą naturalny szum, który dodatkowo zmniejsza prawdopodobieństwo synchronicznego uderzenia.
Idea podejścia — aktualizować pamięć podręczną zanim wygaśnie, całkowicie eliminując chybienie. Proces w tle (cron, scheduler w Kubernetes) okresowo sprawdza popularne klucze i nadpisuje je nowym TTL. Klienci zawsze znajdują pamięć podręczną ważną — stampede jest niemożliwy z definicji.
Time-To-Refresh (TTR) — parametr określający, na ile przed wygaśnięciem rozpocząć aktualizację. Na przykład TTL = 300 sekund, TTR = 60 sekund: na poziomie 240 sekund proces w tle nadpisuje dane. Gwarantuje to, że klienci nigdy nie widzą pustej pamięci podręcznej.
Minus aktualizacji prewencyjnej — stałe obciążenie źródła danych, nawet jeśli nikt nie żąda wpisu. Dla rzadko używanych danych jest to nieefektywne. Rozwiązanie: śledzenie częstotliwości zapytań do klucza (access frequency) i aktualizowanie tylko popularnych wpisów. Adaptacyjny TTR — zaawansowana technika, w której interwał aktualizacji jest obliczany dynamicznie na podstawie historii zapytań.
Blokada mutexowa — odpowiednia dla systemów z umiarkowanym obciążeniem (do 10 000 rps na klucz). Prosta w implementacji i gwarantuje, że źródło otrzymuje tylko jedno zapytanie o aktualizację. Minus — blokady tworzą opóźnienie dla oczekujących wątków.
Probabilistic Early Expiration / XFetch — optymalny dla wysokich obciążeń i systemów rozproszonych. Nie wymaga blokad, skaluje się horyzontalnie, szczytowe obciążenie jest wygładzane naturalnie. Zalecany dla klastrów Redis i Memcached z tysiącami klientów.
Aktualizacja prewencyjna — idealna dla krytycznie ważnych danych z przewidywalnym wzorcem dostępu (konfiguracje, słowniki, podstawowe metadane). Wymaga dodatkowej infrastruktury dla procesu w tle.
Pamięć podręczna na dysku klienta — dla aplikacji mobilnych najważniejszy poziom ochrony. Nawet jeśli pamięć podręczna serwera jest unieważniona, klient mobilny może wyświetlać dane z dysku, podczas gdy nowe zapytanie jest wykonywane. Room + Stale-While-Revalidate — zalecany wzorzec od Google (2025), który całkowicie eliminuje Cache Stampede po stronie klienta.
Często zadawane pytania
Cache Stampede — wynik normalnego zachowania legalnych klientów, którzy jednocześnie odkryli pustą pamięć podręczną. DDoS — celowy atak. W przypadku Stampede wystarczą algorytmy ochronne; w przypadku DDoS potrzebna jest dodatkowa infrastruktura filtrowania ruchu.
Monitoruj wykresy RPS na źródle danych (baza danych, API). Jeśli widzisz regularne szczyty obciążenia zsynchronizowane z momentem wygaśnięcia pamięci podręcznej — to Cache Stampede. Dodaj metrykę cache miss rate dla każdego popularnego klucza.
Tak, jeśli wiele wątków w aplikacji korzysta ze wspólnej pamięci podręcznej in-memory. Na przykład 10 korutyn jednocześnie żąda profilu użytkownika — pierwsza blokuje, pozostałe 9 mogą zduplikować zapytanie. Biblioteka Kotlin kotlinx.coroutines rozwiązuje to przez CoroutineCache lub Flow.distinctUntilChanged.
beta = 1.0 — wartość domyślna, dająca równomierny rozkład przeliczeń. Dla bardziej agresywnej ochrony (mniejsze prawdopodobieństwo chybienia) zwiększaj beta do 1.5–2.0. Dla oszczędzania zasobów źródła — zmniejszaj do 0.5. Zalecany zakres: 0.8–1.2.
Tak, przez mechanizm Cache-Control: stale-while-revalidate i Cache-Control: stale-if-error. CDN zwraca nieaktualną wersję i w tle aktualizuje pamięć podręczną, co jest odpowiednikiem PEE w CDN. Cloudflare i Fastly obsługują te dyrektywy od 2023 roku.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również