Tornado

Tornado - масштабируемый неблокирующий HTTP-сервер на основе epoll, написанный полностью на Python. Изначально он был разработан в рамках проекта FriendFeed, на сегодняшний же день его поддержкой занимается Facebook. Сегодня я хотел бы рассказать о том, как с его помощью можно быстро и легко создавать веб-проекты на Python, которые в дальнейшем будет относительно легко горизонтально масштабировать.

HTTP

Не смотря на приличное количество опциональных модулей, идущих в комплекте с Tornado, проект в первую очередь является именно HTTP-сервером. Используемый механизм epoll (по ссылке можно прочитать о том, в чем он заключается) практически полностью определяет основные принципы работы Tornado:

  • он работает в рамках одного процесса;
  • использование потоков внутри него нежелательно;
  • для использования всех доступных ядер процессора обычно запускают несколько копий одинаковых процессов на разных портах (недавно добавили модуль tornado.process для упрощения реализации этого);
  • обычно обрабатывает HTTP-запросы не напрямую, а через балансировщик нагрузки (nginx или HAProxy).

Эта ситуация мотивирует с самого начала задумываться о распределении нагрузки, а также о выносе выполнения вычислительно сложных задач в отдельные сервисы, скажем конвертирование фото/видео или подсчет какой-то статистики.

Стоит добавить, что вместе с проектом поставляется модуль tornado.wsgi, который позволяет запускать внутри себя другие веб-ориентированные проекты на Python (в частности небезызвестный Django), а также "притворяться" таковым для каких-то внешних серверов или сервисов, которые умеют общаться с Python-приложениями только по WSGI-протоколу, например таковым является Google App Engine. Пользоваться этим модулем крайне не рекомендую, только при постепенном мигрировании проекта с каких-то других технологий.

Обработка запросов

При использовании Tornado не приходится работать с HTTP напрямую - разбор заголовков и URL он берет на себя. От разработчика требуется лишь словарь, состоящий из регулярных выражений и соответствующих им классов-обработчиков запросов.

При создании этих классов настоятельно рекомендую по полной воспользоваться возможностями ООП, в частности наследования. Tornado предоставляет базовый класс RequestHandler, который берет на себя всю грязную работу, а разработчику предлагается реализовать лишь логику, переопределив метод(ы) get, post, delete или head. На практике же обычно удобнее иметь свой собственный базовый класс для обработчиков запросов, который унаследован от RequestHandler и реализовывает общую для текущего конкретного проекта логику (примеры ниже).

Доступ к базе данных

Модуль tornado.database предлагает довольно простой доступ к MySQL. С одной стороны благодаря нему можно сходу начинать разрабатывать приложение на Tornado без использования дополнительных библиотек, с другой - далеко не в каждом проекте используется именно эта СУБД.

В любом случае никто не запрещает использовать любую другую библиотеку для доступа к любой другой СУБД, но есть одно большое НО! Большинство из них являются блокирующими, то есть не возвращают управление до тех пор, пока СУБД не вернет ответ. Почуяли неладное? Правильно, в таком случае весь процесс Tornado, вместе со всеми попавшими в него запросами, будет простаивать пока управление не будет получено обратно, что очень не здорово.

Решается эта неприятная ситуация путем отправки асинхронных запросов к СУБД, то есть после отправки запроса управление сразу же возвращается, а для обработки запроса регистрируется callback, который получит управление, когда прийдет ответ от СУБД. За планирование очередности передачи управления отвечает IOLoop, который и является "сердцем" Tornado.

Ассортимент готовых библиотек, интегрированных с Tornado IOLoop, довольно широк и не ограничивается одним доступом к СУБД. Хотя готовое решение получается найти все же не всегда - приходится возиться с этим всем вручную или мириться с блокировками...

Взаимодействие с внешним миром

В комплекте с Tornado идет неблокирующий HTTP-клиент, так что внутренние сервисы проще всего реализовывать с интерфейсом на JSON over HTTP. Им же можно и обращаться к API внешних сервисов.

С Thrift и Protocol Buffers ситуация несколько более печальна - о прецедентах их интеграции в Tornado IOLoop я не слышал, если кто-то может поделиться информацией - буду благодарен, довольно актуальный вопрос.

Генерация HTML

Шаблонизатор также предлагается свой собственный (не очень удачный, но вполне можно использовать), но его особо никто не навязывает - необходимо лишь переопределить метод render у базового RequestHandler с использованием любого другого аналогичного продукта.

Например, Jinja2, о котором я недавно писал, подключается примерно вот так:

from connections import env
from tornado.web import RequestHandler

class BaseHandler(RequestHandler):
  def render(self, template, context = None):
    if not context: context = {}
    context['user'] = self.current_user
    self.write(env.get_template(template).render(context))
    self.flush()

Прочие бонусы

  • tornado.gen - набор инструментов для упрощения написания асинхронного кода. Благодаря использованию механизма генераторов (yield), позволяет уместить в рамках одного метода и отправку асинхронного запроса и обработку его результата.
  • tornado.websocket предлагает реализацию нескольких последних редакций одноименного протокола,  доступна пара более кроссбраузерных альтернатив с поддержкой нескольких протоколов: sockjs-tornado и TornadIO.
  • С помощью tornado.platform.twisted можно запускать код, написанный под Twisted (несколько более громоздкий и пожилой конкурент), внутри Tornado IOLoop. Актуально для "мигрирующих" проектов и прикручивания библиотек, написанных под Twisted.
  • Без tornado.autoreload разработка превратилась бы в настоящий кошмар.

Заключение

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

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

В комментариях предлагаю подискутировать на тему пригодности Tornado и аналогичных продуктов для использования в различных интернет-проектах, как высоконагруженных, так и маленьких. Ещё мне было бы интересно узнать насколько велик интерес аудитории к чуть более прикладным, чем обычно, статьям, вроде этой - с удовольствием выслушаю Ваше мнение. До новых встреч!

28 февраля 2012 |  Иван Блинков  |  Python