Posty oznaczone etykietą tornado

Kiedy Django zawodzi

Słoneczny dzień

Django jest świetnym rozwiązaniem należącym do kategorii full stack framework. Spójna budowa, setki aplikacji, rozwiązania generyczne (content types framework, comments framework) - batteries included. Świetne narzędzie do realizacji małych i średnich projektów, pozwala zaoszczędzić wiele czasu, nawet gdy istnieje potrzeba napisania od podstaw własnych rozszerzeń. Ostatnio jednak natrafiłem na mur.

Nadciągają cumulusy, czy rozumiesz co to znaczy?

Realizuję zupełnie nową funkcjonalność do portalu górskiego , która generuje sporo żądań XHR. Z rozpędu dodałem widoki w Django i podpiąłem je do szablonów. Rezultat powalił mnie... powolnością odpowiedzi. I to nie chodzi już o benchmarki, tylko o zwykłe odczucia użytkownika.

Szukając rozwiązania udałem się w kierunku Twisted i Tornado. Twisted jest dojrzałym frameworkiem sieciowym sterowanym zdarzeniami, a Tornado młodym web serverem napisanym specjalnie na potrzeby FriendFeed. Obydwa produkty cechuje wysoka wydajność, lecz z racji prostoty Tornado zająłem się nim na pierwszym miejscu. Niestety polubiłem go i mam nadzieję, że kiedyś wrócę do Twisted przy realizacji jakiegoś projektu. Do rzeczy.

Tornado

Tornado wieje z siłą urywającą nie łeb, ale setki głów. Niech poniższy kod (zacytowany ze strony Tornado) posłuży za prosty benchmark i przykład jednocześnie:

import tornado.httpserver
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Wyniki prostego pomiaru na mojej niespecjalnej maszynie:

$ ab -c 1000 -n 10000 http://127.0.0.1:8888/
>> 3208.29 [#/sec]

Zainteresowanych dokładniejszymi testami odsyłam do porównania serwerów asynchronicznych.

Oczywiście docelowo nie będzie tak różowo. Zapytania bazodanowe i kosztowne algorytmy osłabią ten wynik. Krótkie podsumowanie wydajności tej samej funkcji REST API:

  • Django/ORM: 2.47 rq/sec
  • Django/plain db queries (podsystem jako osobny pakiet): 66 rq/sec
  • Tornado/plain db queries (dokladnie to samo, co wyżej): 215 rq/sec (-c100 -n10000, 0 failed)
  • Tornado/plain db queries/in memory cache: 2183 rq/sec (-c100 -n10000, 0 failed)

Django server zapychał się już przy 400 requestach przy concurrency 10.

Jak przetrwać burzę

Jeśli projekt oparłeś w całości na Django, to skazany będziesz albo na refaktoryzację, albo na kombinatorykę pozwalającą importować moduły z aplikacji Django. Ja popełniłem ten błąd i umieściłem API oraz algorytmy w pakiecie zależnym od Django. Na szczęście był to dopiero prototyp i przeniosłem całość do odrębnego pakietu.

Prawidłowe rozwarstwienie podsystemu uczyni go łatwym do integracji. Dobrze jest wydzielić core do oddzielnego pakietu. Nie stosować w nim rozwiązań opierających się na elementach frameworków typu full-stack, szczególnie nie przywiązywać się do warstwy dostępu do danych. Zastosować wzorzec proxy, który ułatwi również opracowanie unit testów. Nie ulegać magii ani kucom na każdym kroku.