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 разработка превратилась бы в настоящий кошмар.

 

 Заключение

 

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

 

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

Вверх