вторник, 23 февраля 2010 г.

Django в неблокирующем стиле, или в погоне за Священным Граалем

Было опубликовано на Хабре.

Присказка

При чтении о Twisted, Tornado, Node.js, у многих python-программистов возникает вопрос - "а вот если взять, и переписать Django в неблокирующем стиле?". Обычный ответ на этот вопрос - нет, не дождетесь. И правда, чтобы переписать целый фреймворк в макаронно-колбечном стиле, надо очень много сил, и большой заряд энтузиазма. Писать с колбеками, очень сомнительное удовольствие.

Так бы и было, но как я писал в своей прошлой заметке, есть в python-мире greenlet-ы, которые легким движением руки помогают скрыть от программиста все эти асинхронные моменты, до определенной степени. В момент написания той заметки, к мысли заставить работать Django в неблокирующем режиме, я относился весьма скептически - ну в самом деле, кто за это возьмется?



Суть

А на самом деле все не так страшно, следите за руками - мы создаем WSGI ресурс в Twisted, начинаем слушать HTTP-запросы; каждый запрос мы отдаем в WSGI-обработчик Django, оборачивая его в гринлет; любые запросы, которые можно преобразовать в неблокирующие, передаем Twisted, который пнет наш гринлет в момент готовности данных; готово!


А что же у нас есть такого, что можно обратить в неблокирующий режим в первую очередь? В Django основным таким местом выступает база данных. Можно ли с ней общаться асинхронно? Абсолютно верно, но нужен специальный драйвер.

С давних пор в Twisted есть такой - PGAsync. У него есть серьезные проблемы, он давно заброшен, но кто нам мешает подхватить упавшее знамя? Я сделал такую попытку, на данный момент драйвер работает стабильнее оригинала, но работа еще не закончена. Тем не менее, асинхронную БД для Django мы получить можем, и это будет Postgresql (ох, простите, пользователи Mysql, но кто вам мешает написать свой?). Драйвер используется в текущем проекте, разрабатываемом Imarto Networks http://imarto.net, и было решено выложить его для свободного использования. Я выложил его раньше готовности, только для демонстрации асинхронного Django, будьте снисходительны.

Чтобы использовать его в Django, нам надо написать свой БД-бэкенд. Я наворотил что-то из бэкенда для psycopg2, обернув обращения к драйверу БД в функции передачи управления главному гринлету, то есть основному потоку программы. Стандартный wsgi-ресурс из twisted.web пришлось тоже немного переделать, ведь он теперь должен кидать запросы на обработку не в пул потоков, а в гринлеты.

Как пощупать

Исходники можно получить тут, там же тестовый django-проект - http://bitbucket.org/deepwalker/tx_green/
Исходники драйвера к БД - http://bitbucket.org/deepwalker/tx-postgresql/

Для использования надо разместить pgasync, green_wsgi.py, tx_green.py, tx_postgresql в место, где интерпретатор сможет их найти. Затем в каталоге test_green_dj настраиваем параметры подключения к БД, ./manage.py syncdb, twistd -n - y server.py. Все, по адресу http://127.0.0.1:8001 встречаем наш тестовый проект.

Про тесты

Тесты, которые я проводил, относились в основном к проверке работы, прироста производительности я не измерял. Если кто-то потратит свое время на это доброе дело, буду благодарен, если поделитесь результатами.

суббота, 9 января 2010 г.

Озеленение Twisted

Как обычно, в праздники, в свободное время от затирки плитки и прочих прибиваний плинтуса, меня захватила очередная идея из цикла "попробовать". На хабре проскочила статья о Pyrant. Первой итерацией я взял и переделал основную часть протокола на Twisted - http://github.com/Deepwalker/tx-tokyo . И все было хорошо, наступление шло по всем фронтам, но тут я перешел к питоничной части pyrant, и понял что сделать yield a[megakey]='mega data string', вообще говоря невозможно. Это было очень печально, ведь в статье меня зацепило именно легкое обращение с данными в питоничной форме. Что же делать, Пух, спросил я себя? И вспомнил о greenlet-ах.

Помучавшись некоторое время, я переписал defer.inlineCallbacks на использование greenlet-ов и смог написать такой код:
from twisted.internet import defer, protocol, reactor
from tx_green import inlineCallbacks
from greentokyo import Tyrant

@inlineCallbacks
def test_proto():
    t = Tyrant()
    print t.get_stats()

    t['kuku'] = 'Green Tyrant!'
    print t['kuku']

    reactor.stop()

if __name__=='__main__':
    test_proto()
    reactor.run()


Удобство в том, что greenlet-ы имеют важное отличие от генераторов - они не ограничены одной функцией. То есть при вызове t['kuku'], на самом деле вызывается t.__getitem__, внутри которого мы и делаем вызов switch() для возврата управления основному циклу на время, пока мы ожидаем данные от сервера. Если бы мы попробовали сделать внутри t.__getitem__ yield, то мы бы просто сделали из него генератор, не получив никакой пользы.

Неудобство состоит в том, что надо помнить обо всей этой начинке - будет плохо если вы будете ждать данные, а вместо этого вам упадет просто Deferred. И будет еще хуже, если вы забудете вернуть управление основному потоку - остановите все приложение. И еще в этом варианте не светит "not twist your brain", потому что надо помнить и использовать возможности Twisted.

Стоит обратить внимание, что в приведенном коде нет ничего, явно указывающего на возврат управления - вызовы greenlet.switch() скрываются в реализации питоничного Tyrant. То есть он заточен под эту реализацию. Если бы это было не так, то нам бы пришлось явно вызывать функцию wait из tx_green. Именно этот момент меня беспокоит - можно создать чуть ли не джангу, и обращения к БД через ORM будут работать также скрытно вызывая wait, но если забыться, то потенциально можно породить трудноуловимую ошибку, или новичок, как обычно не прочитав толком документацию, начнет запрашивать с какого-либо сервера странички через urllib и будет долго удивляться, почему сервер подтормаживает.

Реализация расположена в моей песочнице.

Вот такой небольшой пост. Как всегда ожидаю приятного обсуждения, и надеюсь, что будут хорошие предложения по коду, как было в прошлой нашей встрече, со мной в амплуа постописателя. Спасибо за внимание.