24 lip 2010

Wydajność Django

Najwyższa pora zastanowić się, jak to jest z tą wydajnością w Django. Dla przykładu stworzymy sobie prostą aplikację, która będzie miała za zadanie jedynie generować tymczasowe dane. Dla ułatwienia generujemy tylko i wyłącznie prosty kod html, bez korzystania z szablonów. Naszym celem będzie sprawdzenie wydajności samego Pythona w charakterze szybkości obsługi żądania, wpływ zapytań do bazy danych na wydajność, a wreszcie - przyspieszenie dzięki użyciu memcache. Testy przeprowadzimy na serwerze www wbudowanym do Django. Będziemy mierzyli parametry pracy Django dla 1000 zapytań.

Zadanie 1: widok zwracający statyczny html. Kod funkcji:

def test(request):
return HttpResponse('<html>Hello!</html>')


Uruchamiamy serwer oraz testujemy:

Total transferred: 152000 bytes
HTML transferred: 19000 bytes
Requests per second: 961.41 [#/sec] (mean)
Time per request: 1.040 [ms] (mean)
Time per request: 1.040 [ms] (mean, across all concurrent requests)
Transfer rate: 142.71 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 1 1 0.8 1 22
Waiting: 0 1 0.8 1 21
Total: 1 1 0.8 1 22

Wydajność rzędu ok. 960 req/s jest jak najbardziej dobra. Dla porównania, serwer bochnianin.pl nie wytrzymała najazdu 60000 wejść na dobę (przy założeniu, że ruch koncentruje się max. 8h w ciągu doby, daje to zaledwie ~3 req/s). Jesteśmy zatem 300x do przodu.

Zadanie 2: Widok odwołujący się do bazy danych (jedno zapytanie). Kod funkcji:

def test(request):
wal = Waluta.objects.get(pk='USD')
return HttpResponse('<html>Hello! - %s</html>' % wal.nazwa)


Total transferred: 173000 bytes
HTML transferred: 40000 bytes
Requests per second: 130.48 [#/sec] (mean)
Time per request: 7.664 [ms] (mean)
Time per request: 7.664 [ms] (mean, across all concurrent requests)
Transfer rate: 22.04 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 1
Processing: 6 8 1.4 7 19
Waiting: 4 7 1.4 7 18
Total: 6 8 1.4 7 19

Ilość obsłużonych żądań na sekundę spadła z magicznych okolic 960 req/s do ok. 130 req/s, co jest jednak nadal bardzo dobrym rezultatem. Pamiętajmy jednak, że jest to wynik dla zaledwie jednego prostego zapytania. Teraz będzie ciut trudniej - w kolejnym przykładzie zapytań będzie więcej, a także dojdzie łączenie relacją typu wiele-do-wielu. Zobaczmy, jak sprawdzi się Django.

Zadanie 3: Widok pobierający z bazy danych więcej informacji, łączenie wiele-do-wielu. Kod funkcji:

def test(request):
wal = Waluta.objects.get(pk='USD')
wartosc = WartoscWaluty.objects.get(waluta=wal)
kurs = wartosc.kurs
return HttpResponse('<html>Hello! - %s</html>' % kurs)

Wyniki:

Total transferred: 182000 bytes
HTML transferred: 49000 bytes
Requests per second: 91.41 [#/sec] (mean)
Time per request: 10.940 [ms] (mean)
Time per request: 10.940 [ms] (mean, across all concurrent requests)
Transfer rate: 16.25 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 9 11 1.4 11 25
Waiting: 8 10 1.3 10 24
Total: 9 11 1.4 11 25


Zwiększenie ilości zapytań do bazy danych spowolniło działanie aplikacji o ok. 40 reg/s. Czy to dużo? Dla porównania - standardowo Wordpress generuje ok. 6 req/s. Pamiętajmy jednak, że w naszym przypadku serwujemy bardzo mało danych, a sama aplikacja jest niezwykle prymitywna, nieporównywalna rozmiarowo z Wordpressem. W podanym przeze mnie linku, autor chwali się, że dzięki użyciu mechanizmu cache, udało mu się przyspieszyć aplikację do ok. 395 req/s. Spróbujmy i my. Użyjemy Memcache.

Zadanie 4: Dodajemy cache. Kod funkcji:

@cache_page(60*15)
def test(request):
wal = Waluta.objects.get(pk='USD')
wartosc = WartoscWaluty.objects.get(waluta=wal)
kurs = wartosc.kurs
return HttpResponse('<html>Hello! - %s</html>' % kurs)

Cache ustawiony jest na 15 minut. Test:

Total transferred: 338000 bytes
HTML transferred: 49000 bytes
Requests per second: 605.64 [#/sec] (mean)
Time per request: 1.651 [ms] (mean)
Time per request: 1.651 [ms] (mean, across all concurrent requests)
Transfer rate: 199.91 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 1 2 1.2 1 33
Waiting: 1 1 1.2 1 32
Total: 1 2 1.2 1 33

Wróciliśmy do trzycyfrowego wyniku, osiągając wynik ponad 605 req/s! To ponad 200 req/s szybciej niż najbardziej przyspieszony Wordpress. Kluczowe pytanie: czy my tak naprawdę potrzebujemy aż tak szybkiego softu? Policzmy, zakładając, że serwujemy 8 godzin na dobę naszą stronę z maksymalną szybkością 605 req/s, to w ciągu miesiąca (30 dni) mamy: 522720000 (słownie: pół miliarda) wejść na miesiąc! Sądzę, że taką wydajność muszą mieć serwisy kalibru YouTube. Jeśli zaś nie użyjemy mechanizmu cache i spróbujemy działać przy szybkości 80 req/s, to okaże się, że nadal możemy obsługiwać ilość wejść na poziomie 69120000 (czyli ponad 69 milionów odsłon), co nadal jest doskonałym wynikiem.

Konkluzja: dla szybkości działania serwisu, oprócz sprawności samego oprogramowania generującego kod html ważna jest także przepustowość łącz, efektywność baz danych, siedzących "pod spodem" oraz specyfika systemu operacyjnego, na którym to wszystko działa. Mój mały test pokazuje tylko zmiany wydajności frameworka Django w zależności od ilości zapytań do bazy danych oraz zastosowanych optymalizacji. W zestawieniu [jak dla mnie] brakuje tylko jednego elementu: testu wydajności Django, uruchomionego na interpreterze pochodzącym z projektu unladen-swallow. Wtedy też otrzymalibyśmy miarodajne przełożenie w postaci wzrostu [lub spadku ;)] ilości obsłużonych żądań na sekundę.

0 komentarze:

Prześlij komentarz