Czterokrotny Schemat
Wada występuje w czterech kolejnych, nieatomowych krokach:
// WADA
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Krok 1: sprawdzanie cache. Krok 2: przegapienie. Krok 3: obliczanie. Krok 4: zapis. Wszystkie cztery kroki: nieatomowe. Między krokiem 1 a krokiem 4 dowolna liczba wątków może wykonać krok 1, a wszyscy zobaczą null.
Pułap Idempotencji
Argumentacja chroniąca ten wzorzec: 'dopuszczalne jest, że dwa wątki obliczają i zapisują ten sam wynik. Wynik jest idempotentny. Nie występuje zniekształcenie danych.'
Ta argumentacja: poprawna pod względem poprawności. Fatalna pod względem kosztów.
Na 1000 wątków w sytuacji przegapienia cache: 1000 wątków wykonuje osobno expensiveCompute(key). Jeśli expensiveCompute wywołuje bazę danych, jednocześnie wykonuje się 1000 zapytań do bazy danych. Jeśli dzwoni do usługi zewnętrznej, jednocześnie wykonywane są 1000 żądań HTTP. System produkujący poprawne wyniki zawodzi pod względem kosztów ich produkcji.
Trzy Wyzwalacze
Płonąca Stado wyzwala się, gdy klucz cache przechodzi z ciepłego na zimny jednocześnie dla wielu wątków:
Zimny start: restart usługi z pustym cache. Pierwszy fale zapytań: każdy klucz przegapi. Wszystko oblicza jednocześnie.
Restart usługi: cykliczny restart resetuje cache między instancjami. Ruch przekierowuje się na zimne instancje.
Wygaśnięcie TTL: wysokiej ruchowo klucz wygaśnie. N wątków wszyscy sprawdzają, wszyscy przegapiają, obliczają przed pierwszym wątkiem, który zapisze wynik.
WSZYSTKIE TRZY WYZIWALACZE: KORELACJA Z FALAMI RUCHU. Stado wybucha, gdy ruch się nasila i cache jest zimny. Najgorszy możliwy czas.
Przykład Elasticsearch EnrichCache
Elasticsearch EnrichCache: komentarz dokumentowany brzmi 'celowo niezameczkowany dla prostoty...dopuszczalne jest, że w konkurencji ponownie wpisujemy ten sam klucz/wartość.' Na 10 000 dokumentów na sekundę z zimnym cache enrich: wszystkie 10 000 żądań trafiają do indeksu enrich jednocześnie. Indeks enrich, projektowany dla okresowych zapytań, staje przed 10 000 równoległych zapytań. Klaster destabilizuje się.
Argumentacja idempotencji: poprawna w komentarzu do kodu. Katastrofalna przy 10 000 dokumentów na sekundę.
Związek z MOAD-0001
MOAD-0001 (Wada Wyrzut) tworzy odkrywce O(N²) w systemach o wysokiej przepływności. Naprawienie MOAD-0001 (O(N²) do O(N)) zdezaktywizuje ten stanowisko. Szybszy przepływ wysyła więcej żądań w dół. Cachy w dół, wcześniej chronione przez odkrywce MOAD-0001, teraz otrzymują skokowe zapytania korelowane. MOAD-0005 zaczyna działać w cachach, które nigdy wcześniej się nie wywoływały. Napraw jeden MOAD; zorganizuj drugi.
Co Prawdziwy Pułap Idempotentny Źle Zrozumiał
Komentarz do Elasticsearch przedstawia staranną analizę techniczną, ale zastosowana do błędnego pytania. Idempotentność: rzeczywista cecha wartego analizowania. Pułap: zatrzymywanie analizy na poprawności bez kontynuowania kosztów.
computeIfAbsent & singleflight
Naprawa: spraw, aby sprawdzanie i obliczanie było atomowe. Jeden wątek oblicza. Pozostali wątki czekają na ten wynik.
Java: computeIfAbsent
// WADA: cztery nieatomowe kroki
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// NAPRAWA: atomowe sprawdzanie i obliczanie
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: jeśli klucz nie istnieje, oblicza dokładnie raz, przechowuje, zwraca. Pozostali wątki wywołujące computeIfAbsent dla tego samego klucu czekają na pierwsze obliczenie. Brak N-krotnego obliczenia. Brak nadpływu.
Go: singleflight.Group
var g singleflight.Group
func getOrCompute(key string) (Value, błąd) {
v, err, _ := g.Do(key, func() (interface{}, błąd) {
return expensiveCompute(key)
})
return v.(Value), err
}
singleflight: jeśli obliczenie dla klucza już się wykonuje, wszyscy wywołujący dla tego samego klucza czekają & dzielą się wynikiem. Jedno obliczenie, N czekających, jeden wspólny wynik. Abstrakcja 'flight': odrzucające w trakcie lotu żądania.
Łącze vs singleflight
Prymitywny łącz na klucz serializuje: wątek 1 oblicza, wątek 2 czeka, wątek 3 czeka. Po zakończeniu wątku 1, wątek 2 wchodzi i sprawdza cache (trafi). Wątek 3 wchodzi i sprawdza cache (trafi). N-1 zakupów łączy i odczytów cache.
singleflight deduplicuje: wątek 1 oblicza, wątki 2 do N wszczekają na wynik wątku 1. Brak oddzielnych zakupów łączy. Brak oddzielnych odczytów cache. Jedno obliczenie, jeden wynik, rozdzielony między N czekających. Mniej operacji niż łącz na klucz.
Obydwie zapobiegają nadpływowi. singleflight zapobiega pracy nadmiarowej bardziej kompletnie.
Przepisz wzorzec
Zastosuj poprawkę do konkretnego scenariusza.
// Cachowanie profilu użytkownika w usłudze Java o wysokiej ruchliwości
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // drogie: 50ms zapytanie bazy danych
profileCache.put(userId, profile);
}
return profile;
}
Usługa restartuje się co rano o 2 nad ranem. O 8 rano 10 000 użytkowników żąda swoich profilów jednocześnie.