Python

индекс
250,37

Async Hearts

Некоторое время назад случилось несколько событий, изменивших привычный вид ландшафта веб-разработки на Питоне: Facebook приобрела сервис Friendfeed и сразу же открыла исходный код технологии проекта — http-сервер и микрофреймворк Tornado. Одновременно разработчик Friendfeed опубликовал в своем блоге заметку, в которой привел причины, по которым было решено с нуля разрабатывать собственный асинхронный веб-сервер.

Статья — экскурсия в самое сердце этого и конкурирующего (Twisted.web) проектов, их циклы асинхронной обработки поступающих данных.



Заметка разработчика содержала критику Twisted, популярного фреймворка для построения асинхронных приложений, как неоттестированого и нестабильного; приводились результаты сравнения производительности простого приложения на Twisted.web(подмножество Twisted, специализирующееся на протоколе http и веб-разработке) и Tornado. Естественно, последний в этих тестах оказывался эффективней.

Один из ключевых программистов Twisted не смог остаться в стороне и привел причины, по которым Friendfeed лучше бы не изобретали велосипед и использовали уже имеющийся инструментарий; в следующем же посте указал на другую разработку — Comet-сервер Orbited, который был портирован на Twisted по причинам большей cтабильности и удобства разработки.

С точки зрения веб-разработчика Tornado и Twisted.web не очень сильно отличаются, поскольку являются микрофреймвоками, предоставляющими только самый базовый инструментарий для работы с запросами, авторизацией и так далее, и не могут сравниться с такими гигантами как Django или, если выйти за пределы мира Питона, Ruby on Rails.

Асинхронность



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

Все действия совершаются одним процессом (тредом) в едином цикле, «цикле событий»(event loop), похожем на те, что встречаются во фреймворках для построения интерфейсов.

Производительность



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

Такой цикл присутствует и в Tornado (ioloop), и в Twisted (различные реализации reactor). Попробуем разобраться в каждом из них, определим причины выигрыша по производительности http-сервера Tornado, оценим код и архитектурные решения каждого из асинхронных серверов.

Tornado (ioloop)



Модуль ioloop из Tornado использует по умолчанию механизм epoll для работы с неблокирующими сокетами. Если такового на платформе (собственно, подойдут только Линуксы с версией ядра 2.6 и старше) не предоставляется, то
используется универсальный select.

Реализация главного цикла крайне простая, умещается в пару небольших файлов: epoll.c — обертка для epoll, ioloop.py — реализация цикла.

В epoll.c в функции Питона оборачиваются epoll_create, epoll_ctl, epoll_wait и объявляется модуль epoll. Этот модуль компилируется и используется в случае, если стандартный модуль языка для асинхронной работы с сокетами (модуль select) не поддерживает epoll (не содержит класс epoll).

Итак, сам цикл событий располагается в методе start класса IOLoop модуля ioloop.py. Далее будут приводится части этого метода с несколько расширенными пояснениями:

def start(self):
    self._running = True
    while True:
        # Таймаут по умолчанию между циклами вызовов обработчиков событий
        # позволяет избежать зависания пула событий
        poll_timeout = 0.2

        # Создаем список обработчиков событий
        callbacks = list(self._callbacks)
        for callback in callbacks:
            # Убираем обработчик из списка неиспользованных и выполняем
            if callback in self._callbacks:
                self._callbacks.remove(callback)
                self._run_callback(callback)
	
	# При наличии обработчиков нет необходимости в задержке между циклами
        if self._callbacks:
            poll_timeout = 0.0

	# Если есть обработчики событий, выполняемые с задержкой во времени, и заданное
        # время уже прошло - выполняем такие обработчики. 
        if self._timeouts:
            now = time.time()
            while self._timeouts and self._timeouts[0].deadline <= now:
                timeout = self._timeouts.pop(0)
                self._run_callback(timeout.callback)
            # следующий комплект событий будет собираться либо стандартное время
	    # задержки, либо, если вызвать отложенный обработчик надо раньше, 
            # через время, установленное для этого обработчика
            if self._timeouts:
                milliseconds = self._timeouts[0].deadline - now
                poll_timeout = min(milliseconds, poll_timeout)
	# Если какой-то обработчикв процессе решил остановить работу - выходим из цикла
        if not self._running:
            break

	# Дальше в течение заданного времени времени собираются события пула
        try:
            event_pairs = self._impl.poll(poll_timeout)
        except Exception, e:
            if e.args == (4, "Interrupted system call"):
                logging.warning("Interrupted system call", exc_info=1)
                continue
            else:
                raise

	# Для заданных файловых дескрипторов (сокетов) вытаскиваются события и 
	# с ними вызываются их хэндлеры(к примеру, функции, читающие данные из сокетов - fdopen)
        self._events.update(event_pairs)
        while self._events:
            fd, events = self._events.popitem()
            try:
                self._handlers[fd](fd, events)
            except KeyboardInterrupt:
                raise
            except OSError, e:
                if e[0] == errno.EPIPE:
                    # происходит при потере соединения с клиентом
                    pass
                else:
                    logging.error("Exception in I/O handler for fd %d",
                                  fd, exc_info=True)
            except:
                logging.error("Exception in I/O handler for fd %d",
                              fd, exc_info=True)


Вот, в общем-то, и все. Циклически вызываются отложенные на определенное время (либо один цикл) вызовы и обработчики поступивших событий. Полученные данные обработчиками читаются/пишутся не полностью, а постепенно, через буферы.

В том же простом и лаконичном стиле написаны все остальные уровни фреймворка: http-сервер, обработчики запросов и отдельных соединений.

Twisted (reactor)



Модуль twisted.internet.reactor из фреймворка представляет собой тот самый цикл событий (event loop), который занимается выполнением обработчиков событий и возможных ошибок.

По умолчанию реактор веб-сервера (как и фреймворк в целом) использует механизм ядра select распределения событий для неблокирующих сокетов; этот механизм универсален для платформ Unix и Win32, хотя и несколько уступает по эффективности реакторам на kqueue(FreeBSD) или epoll(только для Linux)

Рассмотрим работу реактора EPollReactor, как аналога основного механизма, используемого в Tornado (ioloop, работающий с epoll).

Реактор содержит несколько словарей, вокруг которых сконцентрирована вся асинхронная логика цикла. Словари объявляются в конструкторе класса:

class EPollReactor(posixbase.PosixReactorBase):
    implements(IReactorFDSet)
    def __init__(self):
        self._poller = _epoll.epoll(1024)
        self._reads = {}
        self._writes = {}
        self._selectables = {}
        posixbase.PosixReactorBase.__init__(self)


Здесь создается сам пул событий (_poller); словари(_reads и _writes), содержащие отображения целых чисел файловых дескрипторов на случайные числа. По сути дела это просто множества дескрипторов для чтения (_reads) и записи (_writes) данных.

Интерес представляет сам цикл асинхронной обработки событий, поэтому опустим описание служебных методов, объявляемых в классе реактора (и его базовом классе).

Итерация выборки событий и их обработки выглядит следующим образом(комментарии переведены и по возможности расширены):

    def doPoll(self, timeout):
        if timeout is None:
            timeout = 1
        # преобразуем задержку итерации (время сбора событий) в миллисекунды
        timeout = int(timeout * 1000) 
    
        try:
            # Число отбираемых событий ограничим количеством отслеживаемых
            # объектов ввода/вывода (число выбрано эвристически)
            # и временем блокировки цикла, переданным в аргументе вызывающей цикл функцией.
            l = self._poller.wait(len(self._selectables), timeout)
    
    
        except IOError, err:
            if err.errno == errno.EINTR:
                return
            # В случае прерывания ожидания сигналом - выходим из итерации;
            # во всех прочих случаях предполагается, что ошибки могли произойти 
	    # только на стороне приложения и стоит передать исключение дальше
            raise
    
        # Если во время сбора событий не произошло никаких ошибок, приступаем
        # к вызову обработчиков событий на дескрипторах.
        _drdw = self._doReadOrWrite
        for fd, event in l:
            try:
                selectable = self._selectables[fd]
            except KeyError:
                pass
            else:
                log.callWithLogger(selectable, _drdw, selectable, fd, event)


Методу реактора self._doReadOrWrite (переименованной в _drdw) передается дескриптор, произошедшее на нем событие и обработчик события (если таковой был найден). Заглянем в сам метод:

    def _doReadOrWrite(self, selectable, fd, event):
        why = None
        inRead = False
        if event & _POLL_DISCONNECTED and not (event & _epoll.IN):
            why = CONNECTION_LOST
        else:
            try:
                if event & _epoll.IN:
                    why = selectable.doRead()
                    inRead = True
                if not why and event & _epoll.OUT:
                    why = selectable.doWrite()
                    inRead = False
                if selectable.fileno() != fd:
                    why = error.ConnectionFdescWentAway(
                          'Filedescriptor went away')
                    inRead = False
            except:
                log.err()
                why = sys.exc_info()[1]
        if why:
            self._disconnectSelectable(selectable, why, inRead)


Здесь обрабатываются события поступления и записи данных из/в дескриптор, происходит обработка ошибок, если таковые имеются.

Таким образом, на самом низком уровне Tornado и Twisted похожи, отличия начинаются на более высоких уровнях абстракции. Разработка от команды Friendfeed делает над циклом всего несколько простых надстроек (HttpStream -> HttpConnection -> HttpServer и прочие). Циклы здесь основываются только на epoll или select.

Twisted Framework же строится на специальных абстракциях (вроде Deferred); его реакторы реализованы для более широкого спектра решений: poll, epoll, select, kqueue(MacOS и freeBSD), пара инструментов под Win32; есть реакторы, встраивающиеся в циклы фреймворков для построения интерфейсов (PyGTK, wxWidgets).

Выводы



Строго говоря, трудно сравнить универальный сетевой фреймворк и специализированное приложение. Код Tornado значительно проще и лаконичней в целом, больше отвечает принципу pythonic. Озадачивает только отсутствие тестов, что в современной разработке считается неприличным.

С другой стороны, Twisted — универсальный инструмент, который при всех своих действительно широких возможностях сохраняет стройность и последовательность; и в этом смысле его можно сравнить с великолепным Qt( в оригинальной реализации для C++). Http-сервер — всего-лишь частный случай его применения. Код большей
части компонентов фреймворка неплохо оттестирован, предоставляется даже собственный инструмент тестирования (Trial).

Естественно, Twisted, как и всякая обощающая система, уступает в производительности специализированной разработке.

Еще одна причина, по которой Twisted уступает в эффективности Tornado и другому высокопроизводительному асинхронному фреймворку Diesel — более развитая обработка ошибок, которая добавляет надежности, но скрадывает заветные RPS.

Итак, главное преимущество Twisted — универсальность. Tornado — производительность.

Что выбрать? Решайте сами. Оба фреймворка предоставляют веб-программисту весьма спартанский набор средств разработки, однозначно уступая в простоте Django и всеобъемлющей полноте Zope; оба — выигрывают в скорости (до 20-30 процентов прироста по сравнению с решениями на Apache).
+50
9 октября 2009, 03:08
42
VlK

комментарии (29)

+5
catdog #
ну блин, апач это совсем не пример для сравнения. у него сам цикл работы тяжелый, без запуска модуля интерпретатора.

ИМХО я не сторонник того что интерпретируемый код должен заниматься статикой или поддержанием большого количества соединений. Для этого есть nginx/lighttpd(!rip)/другое (нужное подчеркнуть). Другой причины использовать ioloop я не вижу.

Схема nginx-fastcgi-wsgi на редкость универсальна и позволяет очень малыми накладными расходами отдавать ооочен много.

Ну если совсем шило в жопе или хитрые задачи то есть более интересные штуки, например для себя я сделал небольшой гейт fastcgi-wsgi на с/++ с встроенным интерпретатором питона. на живом проекте такая штука вполне себе добавляет 30% к rps.

ЗЫ и кстати почему такое странное заглавие Async Hearts? ведь поллинг с подобным обменом это скорее похоже на asynchat питона
0
EXSlim #
lighttpd rip?
Согласен, что lighttpd сейчас выглядит менее привлекательно, т.к. выпускаются только версии с багфиксами. Но(!), lighttpd совсем не умер, а активно переписывается, и текущую версию 2.0 сейчас можно найти в sandbox. Тесты показывают что он стал еще быстее, а стабильный lighttpd2.0 обещают в конце года и стоит ли говорить, что ребята занимаются этим just for fun
--no hollywar
0
ttim #
Мне кажется это был !rip — не мертв :-)
0
spanasik #
Что за гейт, где статья про него? :-)
+1
catdog #
fastpy, статьи не будет пока я его полностью не доведу до стабильного состояния.

сейчас оно есть только в репозитории:
svn co svn://myau.su/fastpy/trunk fastpy

в доках есть информация как собрать его. используется практически так же как и все остальные гейты.
есть несколько примеров (с джангой и стэнд-алон): у меня работает с Werkzeug.

является комбинированным в плане интерпретаторов/трэд-контекстов
0
VlK #
А с чего вы взяли, что эти сервера используются для статики? Рекомендуемый подход — использовать nginx для статики и, скажем, по одному Tornado на ядро процессора для динамической обработки запросов.

Вы говорили про гейт на С++. Сравнение с Apache? Тогда результаты примерно отвечают результатам Tornado или Diesel. А если так, то не удобней ли делать все на каком-то общем уровне, сведя интерфейсы между языками к минимуму (типа wait/create/ctl)? Это позволяет управлять примитивами асинхронной работы с сокетами прямо на уровне языка высокого уровня; удобней писать сам цикл, строить на нем абстракции.

А название… Так, это творческое. Разбираемые в статье циклы событий — сердце любого асинхронного сервера. Поэтому Async Hearts.

0
catdog #
я вот всеже думаю имеет ли здесь смысл асинхронная обработка соединений?

когда нужно просто быстро отдавать статику и обрабатывать входящие соединения(кипалайвы теже) оно просто идеально подходит, но любая работа работа с блокирующими операциями и переключениями трэдконтекстов становится достаточно тяжелой в питоне
0
VlK #
В смысле процессорного времени? Или программирования? Я, если честно, не замерял.

Есть ли вообще смысл связываться напрямую с трэдами и процессами, без фреймворка типа Twisted (кстати, в рамках концепции Deferred он удобно работает с такими вещами)? Общеизвестно, что параллельное программирование — тот еще pain in the ass.

Мораль появления асинхронных серверов в целом: даже классические задачи программирования можно эффективно (даже более эффективно) решать в пределах одного процесса.
0
catdog #
лишний ивент луп не имеет смысла когда ты точно знаешь что отработаешь запрос и отпустишь коннект. как например в случае с моим кодом.

параллельное программирование это бесспорно та еще заноза в жопе и отладка такого кода превращается в нетривиальную игру «угадай где кондишн рэйс», однако сие не применимо к питону только за счет того что в нем есть GIL со всем втекающими и вытекающими последствиями. так что wsgi приложения которые работают с трэдами как правило никогда особых проблем не доставляют. Чего не скажешь о C-API питона и трэд контекстах. особенно доставляет гарбаж коллектор который трудно предсказуем если есть хоть одна ошибка с refcount.

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

Если в вебе спешить некуда, то нафиг тогда вообще использовать легкие сервера?
0
catdog #
ну соответственно там где есть прок использовать низкоуровные штуки. Та же отдача больших файлов, таже отдача видео, то же поддержание большого количества кипалайвовых соединений.
0
VlK #
Большое количество длительных соединений — это как раз Friendfeed с их Tornado.

Погодите. Или я спорю зря? легкие сервера — это что? В общем-то, пара серверов, описанных в статье — самые что ни на есть легкие.
+1
bsdemon #
Очень советую посмотреть на concurrence или gevent. Их event-loop целиком находится в Си (Libevent), и обе библиотеки предоставляют API синхронного стиля на базе корутин (используются greenlet).
0
VlK #
Если верить сайтам, проекты интересные. Насколько понимаю, они довольно молодые?
0
bsdemon #
Думаю не моложе чем Tornado. Тем более, что gevent являестся продолжателем eventlet, который уже давно разрабатывается.
0
VlK #
Ну это да, быть старше Торнадо не сложно :) С другой стороны, у Торнадо есть весьма крутое портфолио, пускай даже в составе всего одного очень нагруженного проекта.
0
funca #
Почему в подобных проектах чаще используется libevent, нежели libev? Если верить этим тестам (http://libev.schmorp.de/bench.html) libev быстрее.
0
VlK #
Потому что livbevent повзрослее будет. А вообще интересное сравнение. Хоть ты садись и пиши на libev что-нибудь :)
0
ESQUELETO #
Спасибо за статью, было интересно! :-)

Результаты ожидаемые, ведь twisted — это все, что угодно, а tornado — [только] web-сервер. Но узнать именно про события, которые с этим связаны, с техническими комментариями — очень круто.
–1
MagaSoft #
Интересно бы сравнить производительность микрофреймворков между собой и фреймворками, чтобы представлять масштабы.
+1
catdog #
скоро будет. как только я до делаю до продакшена свой f-w гейт.
0
VlK #
У Торнадо на сайте есть примеры сравнения с Apache+Django и прочими синхронными штучками.
0
Jekel #
Любопытно, что при сравнении такого рода веб фреймворков всегда забывают о Paste, который тоже дает возможность разрабатывать WSGI приложения.
На основе его работает Pylons.
0
VlK #
Любопытно, что при сравнении циклов асинхронных серверов я забыл вспомнить про сервер, работающий на трэдах?
0
Jekel #
это камень в огород не конкретно вам, у вас довольной интересный обзор для тех кто не в теме ( как я ), за что спасибо, а про вообще, что Paste не везде упоминают :(
0
VlK #
К сожалению, в нашей стране вообще не очень знакомым с разными питоновскими фреймворками. И хотя последнее время Django поднабрал популярности, в целом же — мало кто слышал слова Zope, Plone, Pylons.
0
Deepwalker #
К сожалению в нашем мире любят обобщать, не пойми на каком основании. Если у вас лично мало знакомых питонистов, которые в свое время исследовали представленный вами список, то я вот о себе такого заявить не могу.

P.S. Plone это уже CMS. Я не сомневаюсь, что вы в курсе, но те миллионы серых масс, что никогда не слышали о питоне и его фреймворках, могут быть дезориентированы.
–1
mkevac #
Разве Twisted не может использовать epoll вместо select? Уверен что тогда его производительность сильно возрастет.
0
VlK #
Может, очень даже может. Здесь я как раз-таки epoll-реактор и разбирал.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.