Nikt w obecnych czasach nie wyobraża sobie wydajnego funkcjonowania sklepu internetowego bez cache. Istnieje dużo różnych systemów cache współgrających z platformą Magento (np. memcache, redis cache, varnish, itd.) i zastosowanie odpowiedniego systemu zależne jest od specyfiki sklepu oraz od spodziewanego obciążenia na jaki sklep będzie narażony. Co jednak gdy  przy dużym obciążeniu cache technicznie spełnia swoją rolę, a jednak aplikacja umiera (tak jakby cache nie działał)?

Spotkaliśmy się z takim przypadkiem w kontekście Redis Cache przy jednym z dużych projektów i podzielę się z Wami tą mega ciekawą wiedzą :) Artykuł będzie opisywał  wpierw problem na jaki natrafiliśmy, następnie co wzbudziło naszą ciekawość przy poszukiwaniu przyczyny problemu (w czym leżał rzeczywisty problem), a na końcu jakie jest jego rozwiązanie.

Problem

Na jednym z dużych sklepów które utrzymujemy (blisko 80 000 produktów, komunikacja z wieloma zewnętrznymi systemami, w szczycie blisko 800 requestów na minutę) zaczał występować zagadkowy problem z redis cache. W losowych dniach zaraz przed szczytem następował diametralny spadek wydajności na sklepie.

Podczas analizy sklepu nie zauważyliśmy by był w tym czasie uruchomiony jakiś mocno obciążający skrypt, czasy odpowiedzi zewnętrznych systemów których dane są przetwarzane synchronicznie były w normie, baza danych pracowała wydajnie a czasy odpowiedzi redis cache były praktycznie takie same jak poza szczytem.

Jedyne co wzbudzało naszą uwagę był fakt że zaraz przed zawałem ilość wypychanych kluczy cache wzrastała a gdy osiągnęła wartość ponad 600 na sekundę następował zawał (co oznacza i czym jest spowodowane wypychanie kluczy cache będzie opisane w dalszej części artykułu). Komenda którą można się posłużyć do sprawdzenia ilości wypychanych kluczy z poziomu wiersza poleceń redis to:

redisevicted_keys – liczba eksmitowanych kluczy z powodu przekroczenia max pamięci

Okazało się że problem ustępował w momencie wyczyszczenia cache. Po samej operacji wyczyszczenia wydajność sklepu normowała się powoli ale to ze względu na to że musiała minąć chwila zanim zbudował się podstawowy cache i sklep powrócił do pełnej sprawności. Po odbudowaniu podstawowego cache wydajność sklepu normowała się do zwykłego poziomu.

Poszukiwanie przyczyny problemu

Idąc tropem wypychanych kluczy przeczytaliśmy w dokumentacji Redis, że wypychanie kluczy następuje w momencie zapełnienia całej dostępnej pamięci (redis ma różne polityki zachowań przy zapełnieniu cache, więcej tutaj.

Wybrana przez nas polityka wypychania kluczy (volatile-lru) działa w ten sposób że gdy zabraknie wystarczająco dostępnej pamięci to pobiera losowo kilka kluczy (które mają ustawione czasy życia – pomija te które są „wiecznie żywe”) i usuwa ten który był ostatnio najmniej używany. Czyli generalnie robi miejsce na kolejną daną na którą nie ma już miejsca w cache.

Popatrzmy więc na to wszystko w kontekście naszego problemu…

To że ilość wypychanych kluczy była duża i rosła świadczy o tym że przychodziło coraz więcej nowych danych na których nie było miejsca w cache. Jest to jednak normalne działanie redis i samo w sobie nie było powodem zawału.

Problem jest więc jeszcze głębszy, drążmy go dalej…

Magento dane w Redis zapisuje poprzez dwa typy SET oraz HASH (sam redis posiada więcej typów danych, zainteresowanych odsyłam do dokumentacji.

HASH jest jakby tablicą wielowymiarową w której do zmiennych przypisana jest wartość, którą chcemy zapamiętać, oraz inne potrzebne parametry np. czas życia czy Tagi w których dana wartość występuje. Najczęściej w Magento dane są przechowywane właśnie w formie HASH.

SET natomiast jest odwzorowaniem tablicy jednowymiarowej, w której kolejna dana jest oddzielnym elementem tablicy. Najczęściej w Magento TAG-i są przechowywane w formacie SET. TAG ma zadanie przechowywać klucze cache po to by usuwać określony zbiór danych. Przykład: posiadając TAG_PRODUKT_ID możemy wyczyścić cache tyczący się tylko danego produktu bo będzie on zawierał listę kluczy cache w których występują wszystkie wartości odnoszące się do tego produktu. Inaczej mówiąc, TAG-i grupują wiele wartości w określony zbiór.

Dla lepszego zrozumienia różnicy i relacji między HASH a SET zamieszczam poniższy rysunek:

redis1Źródło: http://info.magento.com/MagentoECG-UsingRedisasaCacheBackendinMagento.pdf

To ile danych aplikacji w cache (typ HASH) jest przypisanych do TAG-ów (typ SET) zależy od programisty i od potrzeb jakie grupy danych z cache będzie chciał usuwać/aktualizować. Dodatkowo istnieje jeszcze globalny TAG o nazwie MAGE który to zawiera wszystkie występujące klucze w cache.

Widzimy więc że poza samymi danymi przechowującymi określone wartości jest w Redis jeszcze dużo nadmiarowych danych nie robiących nic poza samym agregowaniem kluczy cache i podpowiadając już częściowo rozwiązanie zagadki – one były przyczyną zawałów.

Rzeczywisty problem

Tylko dane aplikacji (HASH) wiedzą ile im pozostało do „śmierci” (wygaśnięcia). W momencie „umierania” określonej danej znika tylko ona sama natomiast lista kluczy zawarta w TAG-ach (SET) pozostaje.

Samo wygaśnięcie danych aplikacji (HASH) jest równoznaczne z mechanizmem wypchnięcia w momencie osiągnięcia pełnego cache (przypadek wypychania kluczy opisany wyżej) – tutaj też TAG`i (SET) zawierające klucze tych wartości ciągle są w cache a dodatkowo pojawiają się nowe w związku z pojawieniem się nowych kluczy cache (jeśli nie wystąpiły wcześniej w danym TAG`u).

Konsekwencją powyższego jest to że zaczyna nam spadać ilość cache którą możemy fizycznie dysponować ponieważ przejmują ją TAG-i. Jeżeli teraz do tego dodamy skalę szczytu w której masowo wypychane są dane aplikacji a TAG-i ciągle puchną to okazuje się że zaczynamy mieć „mało cache w cach`u”.

W naszym przypadku skala dochodziła do takiego momentu że redis praktycznie nie robił nic innego jak tylko wypychał wartości, wstawiał nowe na miejsce starych i tylko powiększał TAG-i przez co określona wartość była tylko na moment bo za chwilę Redis ją wypchnął – czyli tak jakby cache w ogóle nie działał chociaż fizycznie robił swoje i to jest właśnie rozwiązanie zagadki :)

Dla potwierdzenia wykresy statystyczne ilości zajmowanej pamięci przez dane aplikacji (HASH) oraz TAG-i (SET) dla cache tuż przed zawałem oraz dla prawidłowego cache:
redis2

Rozwiązanie

Obejściem problemu jest dodanie do harmonogramu (cron) skryptu, który np. raz dziennie będzie oczyszczał zawartość kluczy typu SET (tagi) z nieistniejących już w pamięci nazw kluczy typu HASH (dane aplikacji zapisywane w cache). Przykład podobnego problemu jak u nas wraz ze skryptem jest dostępny tu.

Rozwiązaniem docelowym powinno być zmniejszenie cache do takiego poziomu by nie następowało wypychanie kluczy (ilość pamięci cache nie będzie dochodziła do maksymalnie ustawionej).

Aby tego dokonać należy przeprowadzić refaktoryzację cache tak by zrezygnować z kluczy które może nie są konieczne (bo np. występuje cache na kilku poziomach przez co cache najniższego poziomu nie jest konieczny bo jest już dana wartość z-cach`owana w bloczku nadrzędnym).

Kolejnym krokiem jest manipulowanie czasem życia wartości cache. Być może te czasy są zbyt długie (np. kilka dni), dana wartość nie jest często wykorzystywana i tylko zajmuje cache. Może być też tak że czas życia cache nie jest w ogóle ustawiony przez co dana wartość jest „nieśmiertelna” (nigdy nie wygaśnie) – takich przypadków trzeba unikać.

Warto także ustawić raz dziennie wykonywanie skryptu przytoczonego wcześniej  niezależnie czy są problemy z cache czy nie. Skrypt bowiem czyści nadmiarowe wpisy w TAG-ach kluczy które już nie występują redukując tymsamym niepotrzebne dane.

Wynikiem refaktoryzacji powinna być stabilna praca cache mogąca zmieniać swoje wartości +- kilkaset MB ale nigdy nie dochodząca do maksymalnie ustalonej wartości – by żyło się lepiej :)

Potrzebujesz pomocy w usprawnieniu Twojego e-sklepu? Napisz do nas.