Django, Twisted i WebSockety
Nigdy nie przypuszczałem, że pracowanie nad architekturą rozwiązań po stronie serwera sprawi mi tyle frajdy, a jednak
Pod koniec lutego zaprezentowałem tutaj demo czata (kto sobie przetestował ten miał szczęście — obecnie demo jest wyłączone) ponieważ odkrywałem technologię long polling. Na jej poznawanie jest teraz trochę za późno (o jakieś 5 lat), ale warto było dowiedzieć się ile problemów ona stwarza. Postanowiłem teraz wybiec trochę do przodu i zaimplementować sobie obsługę WebSocketów.
To że powstał ten wpis — oznacza to, że się udało. Moje rozwiązanie okazało się być całkiem fajne i praktyczne. Zamierzam zatem przedstawić ewolucję architektury server-side z której korzystam.
Początki
Ten blog to nie Wordpress — to aplikacja w Django. Na serwerze nie ma Apache'a, jest Nginx — fenomenalny serwer www. Kiedy uruchamiałem wszystko pod koniec grudnia 2010 roku, architektura wyglądała tak:

Stabilność i niesamowita wydajność przy niewielkim użyciu pamięci umożliwił nginx wraz z uwsgi serwującym aplikacje.
Ewolucja...
Aby mieć możliwość pracy z Comet musiałem wykorzystać Nginx Http Push Module. Byłem przy okazji ciekaw jak z tą techologią będzie współpracował haproxy. Ruch https został dodatkowo przepuszczony przez również zmodyfikowany stunnel (nałożona łata umożliwiająca http_x_forwarded_for). Po stronie aplikacji Django konieczne było dodanie własnego autorstwa middleware'a do obsługi http_x_forwared_for.
Long polling wymagał też lekkiego tuningu ustawień haproxy i serwera nginx.

Taka architektura ma jeszcze jeden niesamowicie praktyczny ficzer, który warto w tym momencie wymienić: jeśli jakaś aplikacja pracuje w trybie debugowania (w nginx zamiast uwsgi jako reverse proxy, a aplikacja jest uruchamiana komendą django-admin runserver), a zależy nam na prawdziwym adresie IP — dzięki middleware obsługującemu http_x_forwared_for nie będziemy widzieć 127.0.0.1 tylko prawdziwy adres IP. Było to dla mnie niezwykle przydatne podczas testowania uwierzytelniania oAuth.
Aplikacje działające na serwerze pracowały równie stabilnie i wydajnie, a całe rozwiązanie wymagało jednocześnie bardzo mało pamięci oraz umożliwiło bardziej elastyczne skanowanie.
Jako, że zamierzałem poznać bliżej WebSockety oraz postawiłem sobie za priorytet, aby działały one na tych samych adresach IP i portach (http i https) co sam serwer www (wykorzystanie http-alt jakoś nie wydaje mi się być właściwym rozwiązaniem). Z przeprowadzonych testów ustaliłem, że z już gotowych rozwiązań — taką funkcjonalność oferuje jedynie komercyjny Kaazing WebSocket Gateway.
Wykorzystanie komercyjnego serwera nie wchodziło w grę — bo przecież mogę sobie coś takiego samemu napisać. Konieczne było napisanie czegoś co stało by "od frontu", co byłoby świadome WebSocketów. Ponieważ haproxy niestety nie jest w stanie sobie z tym zadaniem na tą chwilę (i być może nigdy) poradzić — wymagana było wprowadzenie zmian. Najnowszy na tą chwilę draft WebSocketów — hixie 76 — uniemożliwia nawiązanie całego handshake'u przez serwer reverse-proxy. Należało zatem pójść o krok dalej.
Wykorzystanie Twisted
Konieczne było wykorzystanie serwera "event-driven".
Z pomocą przyszedł silnik Twisted oraz znaleziona na githubie biblioteka txWebSocket którą lekko poprawiłem. Tutaj zaczęła się prawdziwa frajda — zacząłem poznawać Twisted. Poszło szybko i sprawnie.
Zanim zabrałem się za WebSockety uruchomiłem reverse-proxy dla obecnych serwisów www (rozszerzając je o http_x_forwarded_for), skonfigurowałem sobie logowanie do poszczególnych facility, dodałem obsługę https i przygotowałem sobie skrypt startowy w oparciu o twistd.
Kiedy WebSockety zaczęły działać, zacząłem zastanawiać się w jaki sposób:
- umożliwić dalszą komunikację z klientami WebSocketów
- odseparować właściwe aplikacje od głównego serwera frontend'owego
Trochę poszukałem, poczytałem i znalazłem RabbitMQ — napisany w erlangu serwer implementujący protokół kolejkowania AMQP. To wystarczyło aby zamknąć krąg, który wygląda następująco:

Kod minimalistycznej aplikacji (wykorzystującej przygotowaną bibliotekę umożliwiającą komunikację z klientami za pośrednictwem WebSocketów poprzez serwer frontendowy) jest krótki i bardzo czytelny — przedstawię go w następnym wpisie. Na tą chwilę aplikacje Twisted pracują w trzeciej warstwie (połączeniowej). Aby pracowały w warstwie czwartej (sesji) wykorzystując cookie sessionid ustawiane przez Django będę musiał wprowadzić kilka drobnych modyfikacji — myślę, że jeden wieczór wystarczy
Aplikacje Twisted są również w stanie łączyć się do bazy za pośrednictwem Django ORM jeśli tylko jest zastosowany w nich model standalone (wystarczy z poziomu aplikacji ustawić odpowiednio DJANGO_SETTINGS_MODULE oraz odpowiednie ścieżki — bułka z masłem).






Komentarze
Ilość komentarzy do wpisu “Django, Twisted i WebSockety”: 2Wow.
Fajnie jest zobaczyć zakrojony na tak szeroką skalę "eksperyment"
Zastanawia mnie tylko sensowność ostatniego rozwiązania. Chodzi mi o stawianie nginx _za_ twisted. Wydaje mi się że wszystkie zalety nginx w takiej konfiguracji wyparują i nie ma już sensu z niego korzystać. Czy nie dało by się podłączyć aplikacji wprost przez reverse-proxy (bez uwsgi)?
Z longpooling masz całkowitą rację, za późno już na wdrażanie tamtej technologii, ale z socketami znów jest odwrotny problem, ogon przestarzałych przeglądarek może zepsuć humor.
Swoją drogą, coraz częściej umieszcza się aplikacje webowe za reverse-proxy, a po raz pierwszy widzę zainteresowanie adresem ip klienta.. Zawsze mnie to zastanawiało.
Fakt — eksperyment
ale udany i rozwojowy 
Jeśli chodzi o tego nginxa — nastawiłem się na rozwój etapami i Twisted miał w ramach www tutaj zastąpić obsługę vhostów. Wyeliminować uwsgi raczej się nie da, bo aplikacje Django powinny produkcyjnie pracować jako wsgi/fastcgi, ale możliwe jest dodanie do Twisted obsługi uwsgi.
Nie wiem tylko na ile wyeliminowanie Nginxa będzie dobrym rozwiązaniem — z dwóch powodów:
- nginx ustawia nagłówki http dotyczące cache'owania, a nie jestem pewien czy Twisted też to robi. Z tych fragmentów kodu Twisted które przeglądałem, obsługa reverse-proxy jest bardzo minimalistyczna (musiałem sobie samemu dopisać obsługę http_x_forwarded_for oraz lekko poprawić nagłówek http przekazujący nazwę vhosta).
- w konfiguracji nginxa poszczególne vhosty zawierają "wyjątki" zawierające definicje urli które mają być serwowane jako content statyczny, a dopiero reszta przekazywana jest do uwsgi.
Co do long pollingu — z perspektywy przeprowadzonych testów — może gdybym wykorzystał Orbited to by to sensownie działało, ale uparłem się na czyste jQuery i nie wyszło to zbyt fajnie.
A adres IP zawsze się przydaje, ot, chociażby kiedy ktoś może dodać content/komentarz do serwisu — warto zapisać też adres IP, który później można poddać geolokacji itp