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ć.
32 czy 64 bity? 21
Parafrazując tytuł jednego z poprzednich postów, ten powinien brzmieć “64 bitom mówimy papa”.
Nie jest żadną tajemnicą, że w Gronie używamy Linuxa. Niczym nowym też nie jest, że serwis stoi na frameworku Django. Podobno jesteśmy największym serwisem używającym tej technologii.
Jednak nie dla wszystkich jest jasne jak wygląda Django od strony serwerowej…
Podczas surfowania po Gronie, Twoje żądanie dotyczące strony grona (request) trafia do jednego z naszych serwerów. Następnie serwer przypisuje obsługę Twojego requesta jednemu z wielu procesów Django. Pojedynczy proces generuje stronę Grona, ale może obsługiwać tylko jedno żądanie na raz. Linuxowcy mogą znaleźć podobieństwo w tym modelu z modelem “prefork” z konfiguracji Apache. Po wygenerowaniu strony proces jest znów dostępny i czeka na kolejne kliknięcia użytkowników.
Wynika z tego, że na pojedynczym serwerze możemy obsłużyć na raz tylko tyle żądań, ile mamy uruchomionych procesów Django1. Niestety, pojedynczy proces zużywa dość dużo pamięci. Aby móc obsługiwać jak najwięcej użytkowników na raz, wypadałoby mieć możliwie dużo uruchomionych procesów. Jednak ich ilość jest limitowana wielkością pamięci operacyjnej.
Aby zwiększyć wydajność (czyli ilość stron które możemy wygenerować), staramy się w miarę możliwości zmniejszać wielkość pojedynczego procesu. W tym celu możemy zmniejszać ilość linii kodu lub optymalizować zużycie pamięci operacyjnej przez proces. Niestety oba zadania są bardzo trudne.
Ostatnio wpadliśmy na inny pomysł.
Nasze serwery używają 64 bitowej wersji linuxa. Administratorzy zdecydowali się na to już dawno temu. Głównym argumentem było to, że zamierzaliśmy używać większej ilości RAMu niż 4GB, oraz lepsza wydajność tej architektury.
Padł pomysł, żeby sprawdzić ile RAMu zaoszczędzimy przenosząc się na “stare” 32 bity. W sumie to w Pythonie całkiem sporo pamięci zajętej jest przez wskaźniki, a właśnie zmniejszenie jej dałoby największy zysk.
Efekt okazał się dużo lepszy niż przewidywaliśmy. W pełni załadowany proces Django na 32 bitach zajmuje około 60MB, czyli o 30% mniej niż na 64 bitach, gdzie jest to 95MB.
Oto przykład “topa” z maszyny 64 bitowej. Kolumna RES pokazuje orientacyjne zużycie pamięci przez proces:VIRT RES SHR S %CPU TIME+ COMMAND 212m 96m 3832 S 0 1:49.61 django '/gallery/3329236/0/' 203m 96m 3120 S 0 1:37.78 django '/gallery/4871149/6/0/' 211m 95m 3832 S 0 1:38.74 django '/mailbox/box/1/' 210m 95m 3824 S 0 1:45.45 django '/gallery/photo/40328359/' 210m 95m 3844 S 0 1:48.43 django '/users/index/' 210m 94m 3848 S 0 1:33.94 django '/users/'A oto, dla porównania, analogiczny z maszyny 32 bitowej:
VIRT RES SHR S %CPU TIME+ COMMAND 68396 62m 3484 S 0 2:06.94 django '/users/433317/' 67716 61m 3488 S 0 1:50.23 django '/gallery/photo/42009962/' 66264 60m 3488 S 0 2:01.94 django '/pub/join/' 65312 59m 3484 S 0 2:08.75 django '/users/' 65148 59m 3508 S 0 1:48.02 django '/users/1533943/friendlist/' 64884 59m 3508 S 0 1:51.48 django '/'
Podsumowując, możemy powiedzieć, że stosowane 64 bitów może nie być takie wspaniałe. Architektura x86_64 niewątpliwie ma wiele zalet, jednak w naszej sytuacji, gdzie ilość zajętego RAMu zaczyna mieć znaczenie, zaczynamy odczuwać jej wady.
Teoretycznie, po całkowitej migracji na 32 bity bylibyśmy w stanie obsługiwać o 1/3 więcej ruchu niż obecnie. Mamy więc o co walczyć.
1 W praktyce nie jest to takie proste, bo mamy także inne ograniczenia, jak na przykład moc procesora.
Wróciliśmy z Europythona 2
Co naprawdę wydarzyło się na Litwie? Poczatek relacji z naszej wyprawy na Europython.
Co naprawdę wydarzyło się na Litwie? Początek relacji z naszej wyprawy na Europython.
Podróż
Zaczęło się od tego, że dowiedzieliśmy się o wybrykach warszawskich chuliganów w Wilnie. Byliśmy przekonani, ze nie pozostanie to bez wpływu na zachowanie celników. :> Na granicy zjawiliśmy się sporo przed czasem, ale na szczęście odprawa przebiegła bez problemów i już rano znaleźliśmy się w hotelu konferencyjnym.
Organizacja
Organizatorzy podzielili wykłady na kilka grup tematycznych, dzięki czemu łatwiej było wybrać coś dla siebie. Wykłady były przerywane “lighting talks” – krótkimi przemowami. Dostępny był też “open space” – taki informatyczny Hyde Park. Dwa dni przed i po konferencji organizowane były sprinty – czyli zespołowe, szybkie programowanie na dany temat, w których, ze względu na brak czasu, nie mogliśmy uczestniczyć. :(
Wrażenia
PyPy
PyPy – python zaimplementowany w pythonie. Dzięki wspanialej grupie developerów ten innowacyjny projekt wszedł w wersje 1.0. Jednym z developerów był nasz rodak – Maciek Fijałkowski, którego poznaliśmy wcześniej na RuPy.
Co ciekawe PyPyowcy korzystają z grantów finansowych UE i chętnie dzielą się doświadczeniami w ich zdobywaniu.
Testy
Wszystko kręciło się wokół testów. Cykle wykładów i open-spaców o wyższości nosetests, nad py.test, nad unittest. Testy równolegle. Testy regresji…
Wygląda na to, że pythonowcy na serio zaczęli traktować XP (PyPy chwali się 100% code coverage).
Twisted
Coraz częściej używany przy dużych projektach – np. fluendo.com używa gstreamera i tej biblioteki aby wysyłać setki megabitow filmów na żywo. Wow.
Systemy kontroli wersji Wojenka miedzy pythonowymi systemami kontroli wersji- subversion – de facto standard
- bzr – za którym stoi Canonical – goście od Ubuntu
- mercurial – rewelacyjnie szybki – mój faworyt
Zope 3
Twórcy Zope 3 chcą spopularyzować swój produkt przez użycie popularnych pythonowych technologii:- WSGI – integracja z istniejącymi standardami
- PythonPaste – szybki start (cos a’la rails lub django-admin)
Mam nadzieje ze niektóre pomysły zope Zope już niedługo trafia do pythonowego mainstreamu (tak jak stało sie z interface).
Ja skoncentrowałem się na cyklach wykładów “Agile Web Development”, “Python Language and Libraries” oraz “Web Related Technologies”.
Pomiary
Ludzie z googla opowiedzieli jak dokładnie prowadzić pomiary aplikacji webowej. Mamy nadzieję zastosować niektóre z ich technik w praktyce – co zaowocuje jeszcze szybszym Gronem.
Lolpython
Jednym z “szybkich przemów” była prezentacja LOLPythona, zaawansowanej odmiany pythona.
IN MAI datetime GIMME date LIKE DATE
SO IM LIKE FIBBING WIT N OK?
LOL ITERATE FIBONACCI TERMS LESS THAN N /LOL
SO GOOD N BIG LIKE EASTERBUNNY
BTW, FIBONACCI LIKE BUNNIES! LOL
U BORROW CHEEZBURGER
U BORROW CHEEZBURGER
I CAN HAZ CHEEZBURGER
HE CAN HAZ CHEEZBURGER
WHILE I CUTE?
I AND HE CAN HAZ HE AND I ALONG WITH HE
IZ HE BIG LIKE N?
KTHXBYE
U BORROW HE
IZ __name__ KINDA LIKE "__main__"?
COMPLAIN "NOW IZ" AND DATE OWN today THING
IZ BIGNESS ARGZ OK KINDA LIKE 1?
N CAN HAS 100
NOPE?
N CAN HAS NUMBR ARGZ LOOK AT 1!!
GIMME EACH I IN UR FIBBING WIT N OK?
VISIBLE I
Szukamy programistów (w temacie : Programista LOLPythona) C.D.N.
W której bohaterowie- poznają samego Guido von Rossuma,
- prezentują grono Pythowca,
- piją litewskie piwo.
Zapraszanie - bez ryzyka 4
W Gronie pojawiła się nowa opcja: zapraszanie znajomych poprzez import kontaktów z konta pocztowego. Pamiętam, że taką funkcjonalność widziałem już jakiś czas temu w pewnym komunikatorze+voip.
Oryginalnie pomyślana w celu wczytywania wyeksportowanych plików z kontaktami, nieco urosła przez ten czas i teraz potrafi więcej. Mianowicie pozwala wpisać swój login i hasło do używanego serwisu pocztowego. Wtedy sam się loguje i wykonuje (czasem mało oczywistą, a dla niektórych trudną) czynność eksportu kontaktów. Oczywiście działa tylko z webmailami, ponieważ protokół POP3 nie do takich rzeczy został pomyślany, a i przez IMAP chyba nikt tego nie zrobił.
Zatem wiadomo już skąd ten post, i po co to wszystko było. Powiem jeszcze, że takiego stopnia upierdliwości nie wykazuje żaden z pozostałych serwisów, a dwa z nich: spółka-córka tpsa oraz pierwiastek o liczbie atomowej 8, mają procedurę logowania się zrobioną w sposób idealny: bez magii i absurdów.
Jednak gdy na jednej stronie widzimy box do logowania sie na inną, stajemy przed dylematem: zaufać im? Ukradną mi hasło? Sprzedadzą adres spamerom? A nawet jeśli nie, to czy sami będą spamować, wiedząc że adres jest na pewno użytkowany?
Oto oficjalna odpowiedź: nigdy. Czas życia wpisanych danych autoryzacyjnych w naszym systemie ogranicza się do odcinka pomiędzy kliknięciem “zaloguj” i przeładowaniem się strony. Są one wysyłane na odpowiednią stronę logowania tak samo, jak gdyby były wysyłane z przeglądarki – nie są więc ani mniej, ani bardziej bezpieczne niż zwykle.
Po pobraniu kontaktów Grono zapomina o wpisanych danych. Jedyne co z nimi robimy, to zaprezentowanie ich użytkownikowi, który sam zdecyduje kogo zaprosić. Tylko do wybranych osób przez użytkownika wysyłane są zaproszenia mechanizmem który działa identycznie jak ten gdy zapraszamy tylko jedną osobę. To samo tyczy się wgrywanych plików z kontaktami, jak i danych w nich zawartych. Możecie czuć się bezpieczni.
Finał wdrożenia nowego systemu plików - RaidRPC 7
W tym tygodniu odpalamy nowy system składowania multimediów, który pozwoli rozszerzyć grono między innymi o filmy i muzykę!
Już w tej chwili trzymamy największą w polskim internecie liczbę fotek (14 milionow).
Grono multimedia – trochę liczb
- 2.5 terabajta danych
- kilkadziesiąt dysków twardych
- 0.01% downtime
# select count(*) from gallery_photos ;
count
----------
13508255
(1 row)
friendnet=# select count(*) from gallery_galleries ;
count
---------
1618839
(1 row)
Grono multimedia – przyszłość
- Filmy
- Muzyka
- Terabajty Waszych danych.
Gdzie to wszystko pomieścić ?
Nasz rozproszony storage, zdolny serwerować setki megabitów ruchu przy użyciu najtańszych maszyn i opensourcowego softu zostanie ostateniecznie wdrożony w tym tygodniu, co da zielone światło dla zupełnie nowych, multimedialnych aplikacji.
Pierwsza wersja storage (umożliwiająca wgrywanie fotek) ujrzała światło dzienne wraz z odsłona galerii gronowych (półtora roku temu).
Kolejna wersja – oparta o autorski protokół RaidRPC – zostanie wdrożona w tym tygodniu.
Co nowego?
RaidRPC jest znacznie szybszy
Kilkaset uploadów plików na sekundę (w zależności od prędkości dysków)
Używa protokołu “WSGI”: http://www.python.org/dev/peps/pep-0333/
Dzięki czemu można używać dowolnego serwera www
Jest całkowicie restowy
Po prostu robimy POST, GET itp – dzięki czemu może być obsługiwany z poziomu przeglądarki www.
Pozwala na wgrywanie dowolnie dużych plików – strumieniowy upload.
Youtube już wysłało pierwsze zapytania ofertowe :)
Posiada większe możliwości introspekcji
Serwer wie co się z nim dzieje. Zawsze.
Awarie wykrywane i usuwane automatycznie (łącznie z powstaniem serwera po padzie).
Administratorzy będą mogli zająć się grą w quake.
KISS
Prostota elegancją XXI wieku.
Utrudnianie czy bezpieczeństwo? 5
Zadanie: zalogować się na konto pocztowe w serwisie onet.pl.
Utrudnienia: zrobić to poprzez ich interfejs www, ale automatycznie (czyli konstruując i wysyłając requesty).
Teoria:
Zwykle logowanie się polega na wypełnieniu formularza na jakiejś stronie, otrzymaniu przekierowania i przejściu na wskazany adres. Tak to działa np. w gronie.
Praktyka:
- Wchodzimy na http://poczta.onet.pl, dostajemy przekierowanie na /login.html.
- Teraz idziemy na sprytnie zamaskowany mechanizm otrzymywania cookiesów sesji, który jest ukryty pod jednym z obrazków.
- Otrzymujamy ciasteczka, postujemy na login.html dane autoryzacyjne. W nich są pola user, username czy login mamy “e”, a zamiast password jest pole “p”.
- Ponowine po paru redirectach wracamy na login.html z nowym ciasteczkiem.
- Ponownie wysłamy dane. Pojawiają się dwa nowe ciasteczka (sesja zaczyna robić się obszerna)
- Udało nam się zalogować.
Podsumowanie
- ilość odwiedzonych stron: 4.
- łączna ilość requestów, wliczając redirecty: 9
- ilość otrzymanych ciasteczek: 7
- dane autoryzacyjne wysłane: 2 razy,
Gdzie tu sens?
Kerni
Początek IT Bloga 8
Wiele osób na hasło grono.net i dział IT robi wielkie oczy. Dział IT liczy na dzień dzisiejszy 25 osób, a to nie koniec, cały czas rekrutujemy. W tym blogu będziemy pisać o tym co dzieje się w serwisie grono.net od strony technicznej. Będziemy pisać dlaczego dana funkcja działa, jak wyglądała nasza praca nad jej prowadzeniem oraz jakie napotkaliśmy przeszkody w jej przygotowaniu . Będziemy także pokazywać co dzieje się w samym dziale IT i jak wygląda nasza praca. Ot taki kolejny blog. Zdjęcie całego działu dalej.

