Memoize - cichy przyjaciel programisty 16
Grono generuje wszystkie strony html całkowicie dynamicznie. W przeciwieństwie do innych serwisów, nie mamy żadnego odwrotnego proxy (reverse proxy). Nie zapamiętujemy w cache wygenerowanych stron, nie zachodzi u nas taka potrzeba. To na czym się skupiamy, to cache'owanie1 surowych informacji pobieranych z bazy danych.
W naszej architekturze zapytanie klienta trafia poprzez Loadbalancer do serwera, który generuje stronę html. Te serwery nazywamy Backendami. Backendy pobierają niezbędne informacje z bazy danych i zwracają wynikową stronę stworzoną dla konkretnego użytkownika.
W takiej architekturze baza danych musi obsłużyć ogromne ilości zapytań. Aby ją odciążyć wyniki zapytań są zapisywane w cache. Jeśli w przyszłości zajdzie potrzeba wykonania takiego samego zapytania, to nie trzeba będzie ponownie pytać bazy. Wystarczy tylko pobrać wynik z cache. Ta technika jest szeroko znana pod nazwą Memoization.
Rozważmy najprostszy przykład:mem_dict = {} def add(a, b): key = "%r %r" % (a,b) if key in mem_dict: # mamy zapamiętane? return mem_dict[key] val = a + b # wylicz wartość mem_dict[key] = val # zapamiętaj return val
W słowniku mem_dict zapamiętywane są wyniki funkcji. Jeśli funkcja będzie wywołana drugi raz z tymi samymi parametrami, to nie będzie konieczne powtórne wykonywanie obliczeń. Wynik zostanie pobrany ze słownika.
W Gronie do przechowywania cache używamy wielu serwerów Memcached. Nasze serwery Memcached są traktowane przez programistę jako gigantyczny rozproszony słownik, w którym dane są trzymane w pamięci wielu maszyn.
Stosowanie Memcached to dość standardowa praktyka. Używa go wiele serwisów, na przykład Facebook.
Jednak standardowy serwer Memcached udostępnia jedynie podstawowe funkcje, których trudno jest używać w naszym środowisku. Dlatego stworzyliśmy programistyczny interfejs do cache'owania o nazwie Memoize, który znacząco upraszcza używanie cache (jego głównym autorem jest Marek Pułczyński).
Kolejnym przykładem będzie funkcja, która pobiera pewne informacje z bazy danych, na przykład dane użytkownika. Standardowo w Django wyglądałoby to mniej więcej tak:
from models import User def get_user(uid): user = User.objects.get(id=uid) return user
Spróbujmy użyć dekoratora Memoize, aby wynik tej funkcji był pobierany z cache:
from models import User from grono2.contrib.memoize import memoize def user_key(uid): return 'model.user.%i' % (uid) @memoize(user_key) def get_user(uid): user = User.objects.get(id=uid) return user
Funkcja user_key generuje klucz, pod którym zostanie umieszczony element w cache. Moża by przypuszczć, że klucz do obiektu można wygenerować automatycznie z parametrów funkcji. Jednak funkcja generująca klucz została wyodrębniona, gdyż może zaistnieć przypadek, że niektóre parametry cache’owanej funkcji nie są jej kluczami głównymi.
Na przykład, poniższy parametr print_debug nie wpływa w żaden sposób na stan cache'owanego obiektu, dlatego nie powinien być brany pod uwagę podczas generowania klucza:
@memoize(user_key2) def get_user_with_warning(uid, print_debug=True): user = User.objects.get(id=uid) if user.name == "Jozin" and print_debug: print "Jozin pobrany z bazy." return user
Jednak co się stanie, gdy zmienimy coś na obiekcie User? W cache będzie przecież stara wartość, więc kolejne wywołania funkcji będą zwracać niewłaściwe stare dane. Dlatego programista musi explicite usunąć (unieważnić, zinwalidować) starą wartość z cache.
Realizowane jest to następująco:from grono2.contrib.memoize import invalidate def change_user_name(uid, new_name): user = get_user(uid) user.name = new_name user.save() invalidate(user_key(uid)) # wyczyść stary cache
Zajmijmy się trudniejszym przykładem, gdzie dwa obiekty połączone są relacją. Przy zmianie jednego może zaistnieć potrzeba zinwalidowania drugiego. Nasza implementacja Memoize pozwala na to.
W tym przykładzie niech model Profile będzie powiązany relacją z modelem User:from models import User, Profile from grono2.contrib.memoize import memoize def user_key(uid): return 'model.user.%i' % (uid) @memoize(user_key) def get_user(uid): user = User.objects.get(id=uid) return user def profile_key(uid): return 'model.profile.%i' % (uid) @memoize(profile_key) def get_profile(uid): p = Profile.objects.get(id=uid) p.user = get_user(id=uid) return p
Jak widać zmiana w User musi spowodować unieważnienie cache obiektu Profile. Aby to osiągnąć definiujemy klucz wiążący, u nas nazywany kluczem wersji. Następnie, aby usunąć wszystkie powiązane rekordy wystarczy jedynie zinwalidować ten klucz.
def super_key(uid): return "super_key.%d" % uid @memoize(user_key, super_key) def get_user(uid): ... @memoize(profile_key, super_key) def get_profile(uid): ... def change_user_name(uid, new_name): user = get_user(uid) user.name = new_name user.save() invalidatev(super_key(uid)) # usuń powiązane rekordy z cache
Wygląda to prosto i intuicyjnie. Jednak główna trudność polega na tym, że działamy w środowisku rozproszonym. Jest możliwa sytuacja, gdy jeden Backend zmodyfikuje rekord i go usunie z cache, a inny Backend pobierze chwilę wcześniej starą wartość i doda ją do cache. Efekt tego będzie taki, że w cache będzie przechowywana niepoprawna wartość. Na szczęście, dzięki przemyślanej implementacji Memoize mamy gwarancję, że taka sytuacja nie wystąpi.
Opisana architektura bardzo dobrze sprawdza się w praktyce. Serwery Memcached bardzo łatwo się skalują, gdy brakuje nam pamięci na cache, to po prostu dostawiamy kolejną maszynę.
Ciekawskich może zainteresować to, że raz na jakiś czas musimy zrestartować serwery Memcached. Zanim cache ponownie zostanie wypełniony danymi mija co najmniej doba.
Z punktu widzenia programisty dekorator Memoize można bardzo prosto stosować w praktyce. Nie jest to żadna przełomowa technologia, lecz łatwość w nauce i zastosowaniu powoduje że nawet młodsi informatycy mogą pisać kod, który będzie działał dla dziesiątków tysięcy użytkowników.
Dzięki cache'owaniu prawie 99% wszystkich zapytań nie musi trafić do bazy. Jednak cały czas staramy się znaleźć miejsca, które nie są poprawnie cache’owane, aby móc jeszcze bardziej przyspieszyć serwis i odciążyć bazę danych.
1Przepraszam purystów językowych za polonizowanie wyrazu cache. Jak to często w informatyce bywa, nie istnieje jeszcze dobry odpowiednik tego wyrazu w języku polskim. Artykuł Wikipedii sugeruje odmianę z apostrofem i postaram się tego trzymać.
Trackbacks
Use the following link to trackback from your own site:
http://itblog.grono.net/articles/trackback/56
-
Ostatnio szukalem firmy w warszawie ktora zajmuje sie spawaniem konstrukcji metalowych i znalazlem firme maroma. Nie wiem czemu firma na stronie nie chwali sie zbytnio tym ze oferuje konstrukcje spawane, ale po kontakcie z nimi dowiedzialem sie ze...

Na podobnej zasadzie dziala rozwiazanie systemu transakcyjnego w ubs ag i swissquote
Przez lata wypracowalismy wlasne middleware podobne w duzej mierze do JBoss’a zwane JMaster’em zoptymalizowane w duzej mierze do uzycia w srodowisku mainframe.
Tak nieco obok tematu zawsze mnie zastanawiało czemu na gronie nie ma opcji edytowania posta. Ostatnimi czasy zaczął się pojawiać. Z treści posta może to wynika, ale ja wolę zapytać wprost: Czy ma to coś wspólnego z cache’owaniem w gronie?
a co ma piernik do wiatraka?
Artur Kulig: Czy ma to coś wspólnego z cache’owaniem w gronie?
Nie. Chodzi o obciążenie bazy. Opisana architektura jest bardzo dobrze skalowalna jeśli chodzi o odczyty z bazy danych, jednak w żaden sposób nie wpływa na zapisy do bazy. Edycja postu to właśnie zapis. Czasem baza jest zbyt mocno obciążona i wtedy najłatwiejszą rzeczą jaką można zrobić żeby zmniejszyć zapisy, to właśnie wyłączenie edycji i usuwania postów.
fajnie, ze dzielicie sie pewnymi informacjami z userami. w przeciwienstwie do allegro ekipa grona jest stosunkowo tworcza – oby tak dalej.
z wlasnego prywatnego doswiadczenia wiem, ze odpowiednio zaprojektowana farma reverse proxy moze uchronic przed skutkami przykrych atakow ddos.
co ekipa grona ma do powiedzenia w tej kwestii?
nie mowie tu o fakcie, ze takowy atak mial miejsce czy nie namawiam do przyznania sie bo nie taki cel tego.
probowaliscie zapuszczac odpowiednie narzedzia (hping3) np. po LAN’ie i sprawdzic kiedy nastapi ugiecie systemu?
maxim: hping3?? a czy on potrafi generowac zapytania HTTP? chyba nie :):P wiec to chyba nie ta warstwa,ktora jest opisana w tym artykule:)
moim zdaniem takie cos latwo mozna zablokowac na samych balancerach (nakladajacs limity na polaczenie z danego adresu ip) lub wczesniej na routerach …itd.
wiec pewnie to co wg Ciebie robi “farma reverse proxy”, jest zalatwiane moim zdaniem wlasnie przez przez balancer.
mnie tylko ciekawi, jesli to nie tajemnica, ile % trafien jest w memcached?
pozdrowienia, Tomek
tn: ile % trafien jest w memcached?
Tak jak napisałem, prawie 99%. Chyba nie ma większego znaczenia czy to będzie 89.8% czy 99.6%. Jak widać praktycznie wszystkie zapytania trafiają w memcached.
tn – hping3 nie musi obslugiwac protokolu http aby pozamiatac maly serwer http czy http load balancera a wlasciwie dowolna aplikacje serwerowa oparta na tcp
pod warunkiem oczywiscie, ze stojacy na wejsciu sprzet jest niewlasciwie skonfigurowany
sami borykalismy sie z problemem w sumie dosc skromnego ddos’a (ok. 300 Mb/s) ktory skutecznie utrudnial zycie paru naszym klientom.
nalozenie dosc sensownych limitow jest zawsze problemem tak aby nie utrudnialy zycia klientom a jednoczesnie dawaly sensowny efekt.
z pewnoscia grono stosuje ciscowe gsr’y – soft z opcja security czy anomaly detectory chociaz watpie aby siegali po te ostatnie z racji dosc podrasowanej ceny (cirka > 40 k$). ewentualnie starsze PIX’y czy nowe ASA.
najprostsza opcja – postawic linucha 2.6 jako most na sensownym sprzecie i do tego iptables + ew. pare skrypcikow. skutecznosc zalezy od ustawien i rodzaju ataku.
przy okolo 300 regolach iptables na sprzecie z prockiem 3GHz + 1 GB ram’u + ruch na poziomie 400-500 Mbit/s obciazenie sprzetu w granicach 15-18%.
coby byc pelnoslownym:
hping3—flood—rand-source—syn atakowany_host
przetestujta sobie w siakims lan’ie
publikuje tylko w celach edukacyjnych
jak chcesz uzyc tego do robienia dymowicha, to palnij sie lepiej w czache tudziez odwiedz klinike psychiatryczna
max
Interesujące, spróbuję wykorzystać w swoich serwisach. Pozdrawiam.
maxim: no nie musi, ale tematem postu jest memcached, a nie obrona przed (D)DOS.
co do reszy to z tego co pamietam to PIX i ASA to chyba nie na ten kaliber jesli chodzi o przepustowosc…:) jesli juz to modul FWSM do jakiegos 6500/7200.
co to tego ataku via hping3 to mozesz odpalic against OpenBSD :) efekt mizerny, wiec doswiadczenie mam troche inne, a warunki wejsciowe byly podobne:)
kolejny pseudoinformatyk do kolekcji
no to jest nas dwoch, tyle ze ja nie jestem pryszczaty:P
a skad wiesz, ze mam morde obrosnieta syfami? sprawdzales?
Korzystacie z jednej bazy danych? Czy też macie jakieś swoje rozwiązanie tej kwestii (gdyż Django ORM nie wspiera wielu baz, niestety)?
Dla większości danych tak – jedna baza. Jednak naprawdę duże tabele są przechowywane na dedykowanych maszynach, często tabele są ręcznie partycjonowane pomiędzy maszynami. Depesz opisywał jedną z migracji takich tabel na Postgresa: http://itblog.grono.net/articles/2007/12/10/baza1