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).
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 29
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        lighttpd rip?
        Согласен, что lighttpd сейчас выглядит менее привлекательно, т.к. выпускаются только версии с багфиксами. Но(!), lighttpd совсем не умер, а активно переписывается, и текущую версию 2.0 сейчас можно найти в sandbox. Тесты показывают что он стал еще быстее, а стабильный lighttpd2.0 обещают в конце года и стоит ли говорить, что ребята занимаются этим just for fun
        --no hollywar
        • 0
          Мне кажется это был !rip — не мертв :-)
        • 0
          Что за гейт, где статья про него? :-)
          • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            А с чего вы взяли, что эти сервера используются для статики? Рекомендуемый подход — использовать nginx для статики и, скажем, по одному Tornado на ядро процессора для динамической обработки запросов.

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

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

            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                В смысле процессорного времени? Или программирования? Я, если честно, не замерял.

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

                Мораль появления асинхронных серверов в целом: даже классические задачи программирования можно эффективно (даже более эффективно) решать в пределах одного процесса.
                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Камни не камни, а сервера написаны и работают, быстро работают. Инструментов много, и они показали свою работоспособность. Странно теперь говорить о неэффективности такого подхода.

                    Если в вебе спешить некуда, то нафиг тогда вообще использовать легкие сервера?
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        Большое количество длительных соединений — это как раз Friendfeed с их Tornado.

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

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

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

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