Panika w pamięci podręcznej

Awaria pamięci podręcznej to rodzaj kaskadowej awarii , która może wystąpić, gdy masowo równoległe systemy obliczeniowe z mechanizmami buforowania znajdują się pod bardzo dużym obciążeniem. To zachowanie jest czasami nazywane również dog-pilingiem .

Aby zrozumieć, w jaki sposób pojawiają się błędy w pamięci podręcznej, rozważ serwer WWW , który używa memcached do buforowania renderowanych stron przez pewien czas, aby zmniejszyć obciążenie systemu. Przy szczególnie dużym obciążeniu pojedynczego adresu URL system pozostaje responsywny, dopóki zasób pozostaje w pamięci podręcznej, a żądania są obsługiwane przez dostęp do kopii w pamięci podręcznej. Minimalizuje to kosztowną operację renderowania.

Przy niskim obciążeniu chybienia w pamięci podręcznej skutkują pojedynczym ponownym obliczeniem operacji renderowania. System będzie działał tak jak poprzednio, przy czym średnie obciążenie będzie utrzymywane na bardzo niskim poziomie ze względu na wysoki wskaźnik trafień w pamięci podręcznej.

Jednak przy bardzo dużym obciążeniu, gdy wygaśnie wersja tej strony w pamięci podręcznej, w farmie serwerów może istnieć wystarczająca współbieżność , aby wiele wątków wykonawczych próbowało jednocześnie renderować zawartość tej strony. Systematycznie żaden z współbieżnych serwerów nie wie, że pozostałe wykonują to samo renderowanie w tym samym czasie. Jeśli występuje wystarczająco duże obciążenie, może to samo w sobie wystarczyć, aby doprowadzić do załamania przeciążenia systemu poprzez wyczerpanie współdzielonych zasobów. Upadek zatorów powoduje, że strona nigdy nie zostanie całkowicie ponownie wyrenderowana i ponownie umieszczona w pamięci podręcznej, ponieważ każda próba wykonania tego kończy się przekroczeniem limitu czasu. W ten sposób panika w pamięci podręcznej zmniejsza współczynnik trafień w pamięci podręcznej do zera i utrzymuje system w stanie ciągłego załamania, gdy próbuje zregenerować zasób tak długo, jak obciążenie pozostaje bardzo duże.

Aby podać konkretny przykład, załóżmy, że renderowanie rozpatrywanej strony zajmuje 3 sekundy, a ruch wynosi 10 żądań na sekundę. Następnie, gdy strona z pamięci podręcznej wygaśnie, mamy 30 procesów, które jednocześnie ponownie obliczają renderowanie strony i aktualizują pamięć podręczną o wyrenderowaną stronę.

Typowe użycie pamięci podręcznej

Poniżej znajduje się typowy wzorzec użycia pamięci podręcznej dla elementu, który musi być aktualizowany co ttl jednostki czasu:

  funkcja  fetch(  klucz  ,  ttl  ) {  wartość  ← cache_read(  klucz  )  if  (!  wartość  ) {  wartość  ← recompute_value() cache_write(  klucz  ,  wartość  ,  ttl  ) }  zwracana  wartość  } 

Jeśli funkcja recompute_value() zajmuje dużo czasu, a dostęp do klucza jest często uzyskiwany, wiele procesów jednocześnie wywoła funkcję recompute_value() po wygaśnięciu wartości pamięci podręcznej.

W typowych aplikacjach internetowych funkcja recompute_value() może wysyłać zapytania do bazy danych, uzyskiwać dostęp do innych usług lub wykonywać skomplikowane operacje (dlatego to konkretne obliczenie jest w pierwszej kolejności buforowane). Gdy wskaźnik żądań jest wysoki, baza danych (lub jakikolwiek inny udostępniony zasób) będzie cierpieć z powodu przeciążenia żądań/zapytań, co z kolei może spowodować załamanie systemu.

Łagodzenie paniki w pamięci podręcznej

Zaproponowano kilka podejść w celu złagodzenia paniki w pamięci podręcznej (znanej również jako zapobieganie gromadzeniu się psów). Można je z grubsza podzielić na 3 główne kategorie.

Zamykający

Aby zapobiec wielokrotnym jednoczesnym ponownym obliczeniom tej samej wartości, po chybieniu pamięci podręcznej proces spróbuje uzyskać blokadę dla tego klucza pamięci podręcznej i obliczy ją ponownie tylko wtedy, gdy ją uzyska.

Istnieją różne opcje implementacji dla przypadku, gdy blokada nie jest nabyta:

  • Poczekaj, aż wartość zostanie ponownie obliczona
  • Zwróć „nie znaleziono” i poproś klienta, aby poprawnie obsłużył brak wartości
  • Zachowaj przestarzały element w pamięci podręcznej do użycia podczas ponownego obliczania nowej wartości

Prawidłowo zaimplementowane blokowanie może całkowicie zapobiec panice, ale wymaga dodatkowego zapisu dla mechanizmu blokującego. Oprócz podwojenia liczby zapisów, główną wadą jest poprawna implementacja mechanizmu blokującego, który zajmuje się również przypadkami brzegowymi, w tym awarią procesu uzyskiwania blokady, dostrojeniem czasu życia blokady, warunkami wyścigu , i tak dalej.

Przeliczenie zewnętrzne

To rozwiązanie przenosi ponowne obliczenie wartości pamięci podręcznej z procesów, które jej potrzebują, do procesu zewnętrznego. Ponowne obliczenie procesu zewnętrznego można uruchomić na różne sposoby:

  • Gdy wartość pamięci podręcznej zbliża się do wygaśnięcia
  • Cyklicznie
  • Gdy proces wymagający tej wartości napotka błąd w pamięci podręcznej

Takie podejście wymaga jeszcze jednej ruchomej części – procesu zewnętrznego – który musi być utrzymywany i monitorowany. Ponadto rozwiązanie to wymaga nienaturalnej separacji/duplikacji kodu i jest najbardziej odpowiednie dla statycznych kluczy pamięci podręcznej (tj. nie generowanych dynamicznie, jak w przypadku kluczy indeksowanych przez identyfikator).

Probabilistyczny przedterminowy termin ważności

Przy takim podejściu każdy proces może przeliczyć wartość pamięci podręcznej przed jej wygaśnięciem, podejmując niezależną decyzję probabilistyczną, w której prawdopodobieństwo wykonania wczesnego przeliczenia wzrasta, gdy zbliżamy się do wygaśnięcia wartości. Ponieważ decyzja probabilistyczna jest podejmowana niezależnie przez każdy proces, efekt paniki jest łagodzony, ponieważ mniej procesów wygasa w tym samym czasie.

Wykazano, że następująca implementacja oparta na rozkładzie wykładniczym jest optymalna pod względem skuteczności w zapobieganiu panice i tego, jak mogą wystąpić wczesne ponowne obliczenia.

  funkcja  x-fetch(  klucz  ,  ttl  ,  beta  =1) {  wartość  ,  delta  ,  wygaśnięcie  ← cache_read(  klucz  )  if  (!  wartość  || (czas() -  delta  *  beta  * log(rand(0,1))) ≥  wygaśnięcie  ) {  początek  ← czas()  wartość  ← recompute_value()  delta  ← czas() – start cache_write(  klucz  , (  wartość  ,  delta  ),  ttl  ) }  zwracana  wartość  } 

Parametr beta można ustawić na wartość większą niż 1, aby faworyzować wcześniejsze ponowne obliczenia i jeszcze bardziej zredukować popłochy, ale autorzy pokazują, że ustawienie beta = 1 dobrze sprawdza się w praktyce. Zmienna delta reprezentuje czas potrzebny do ponownego obliczenia wartości i służy do odpowiedniego skalowania rozkładu prawdopodobieństwa.

Podejście to jest proste do wdrożenia i skutecznie zmniejsza ilość pamięci podręcznej, automatycznie faworyzując wczesne ponowne obliczenia, gdy wzrasta natężenie ruchu. Jedną wadą jest to, że zajmuje więcej pamięci w pamięci podręcznej, ponieważ musimy spakować wartość delta z elementem pamięci podręcznej - gdy system buforowania nie obsługuje pobierania czasu wygaśnięcia klucza, musimy również przechowywać wygaśnięcie ( to znaczy czas ( ) + ttl ) w pakiecie.

Linki zewnętrzne