Tornado - фреймворк асинхронного типа

Основы асинхронности в Python и цикл ввода-вывода

Tornado, по большей части, такой же чистый фреймворк, как и Flask, но с существенным отличием: Tornado создан специально для обработки асинхронных процессов.

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

Основные проблемы асинхронной программы:

  1. Как поступают данные?
  2. Как идут данные?
  3. Когда можно запустить какую-либо процедуру, которая не занимала бы моего полного внимания?

Из-за глобальной блокировки интерпретатора (GIL) Python по своей природе является однопоточным языком. Для каждой задачи, которую должна выполнить программа Python, все внимание потока ее выполнения сосредоточено на этой задаче на протяжении всего выполнения. Наш HTTP-сервер написан на Python. Таким образом, когда принимаются данные (например, HTTP-запрос), единственным объектом внимания сервера являются эти входящие данные. Это означает, что в большинстве случаев любые процедуры, которые должны выполняться при обработке этих данных, будут полностью поглощать поток выполнения вашего сервера, блокируя получение других потенциальных данных до тех пор, пока ваш сервер не завершит все, что ему нужно сделать.

Во многих случаях это не слишком проблематично; типичный цикл веб-запроса-ответа займет всего доли секунды. Наряду с этим сокеты, из которых построены HTTP-серверы, могут поддерживать резервирование входящих запросов для обработки. Таким образом, если запрос приходит, пока этот сокет обрабатывает что-то еще, есть вероятность, что он просто подождет в очереди, прежде чем будет обработан. Для сайта с низким и промежуточным трафиком доля секунды не так уж и важна, и вы можете использовать несколько развернутых экземпляров вместе с балансировщиком нагрузки, таким как NGINX, для распределения трафика больших запросов.

Однако, что если ваше среднее время отклика занимает более доли секунды? Что, если вы используете данные из входящего запроса, чтобы запустить какой-то длительный процесс, такой как алгоритм машинного обучения или какой-то массивный запрос к базе данных? Теперь ваш однопоточный веб-сервер начинает накапливать неадресуемый список невыполненных запросов, некоторые из которых будут исключены из-за простого тайм-аута. Это не вариант, особенно если вы хотите, чтобы ваш сервис считался надежным на регулярной основе.

Тут-то вам и пригодится асинхронная программа Python. Важно помнить, что, поскольку она написана на Python, программа все еще является однопоточным процессом. Все, что блокирует выполнение в синхронной программе, если оно специально не помечено, все равно будет блокировать выполнение в асинхронной.

Однако, если структура структурирована правильно, ваша асинхронная программа на Python может «откладывать» долгосрочные задачи, когда вы указываете, что определенная функция должна иметь возможность это делать. Затем ваш асинхронный контроллер может быть предупрежден, когда отложенные задачи завершены и готовы к возобновлению, управляя их выполнением только тогда, когда это необходимо, без полной блокировки обработки нового ввода.

Так где вводится цикл ввода/вывода?

Асинхронная программа Python работает, принимая данные из некоторого внешнего источника (входные данные) и, если процесс требует этого, выгружает эти данные некоторым внешним работникам (выходные данные) для обработки. Когда этот внешний процесс завершается, основная программа Python получает предупреждение. Затем программа выбирает результат этой внешней обработки (ввода) и продолжает свой веселый путь.

Всякий раз, когда эти данные не находятся в руках основной программы Python, эта основная программа может работать практически над чем-либо еще. Сюда входит ожидание совершенно новых входных данных (например, HTTP-запросов) и обработка результатов длительных процессов (например, результатов алгоритмов машинного обучения, длительных запросов к базе данных). Основная программа, хотя и остается однопоточной, становится управляемой событиями и приводится в действие для конкретных случаев, обрабатываемых программой. Основным работником, который прослушивает эти события и диктует, как их следует обрабатывать, является цикл ввода-вывода.

Я знаю, что мы прошли долгий путь, чтобы найти это объяснения, но я надеюсь сообщить здесь, что это не волшебство и не сложная параллельная обработка или многопоточная работа. Глобальная блокировка интерпретатора все еще на месте; любой длительный процесс в основной программе все равно будет блокировать что-либо еще. Программа также все еще однопоточная; тем не менее, извлекая утомительную работу, мы сохраняем внимание этого потока только к тому, к чему он должен быть внимательным.

Маршрутизация Tornado

Несмотря на все трудности разговоров об асинхронности в Python, мы немного остановимся на ее использовании и опишем базовое представление Tornado.

В отличие от представлений на основе функций фреймворков (вроде Flask и Pyramid), представления Tornado основаны на классах. Это означает, что мы больше не будем использовать отдельные, автономные функции для определения порядка обработки запросов. Вместо этого входящий HTTP-запрос будет перехвачен и назначен атрибутом нашего определенного класса. Затем его методы будут обрабатывать соответствующие типы запросов.

Давайте начнем с основного представления, которое выводит на экран "Hello, World". Каждое основанное на классе представление, которое мы создаем для нашего приложения Tornado, должно наследоваться от объекта RequestHandler, найденного в tornado.web. Это создаст всю базовую логику, которая нам понадобится, чтобы принять запрос и построить правильно отформатированный ответ HTTP.

Поскольку мы стремимся обработать запрос GET, мы объявляем (действительно переопределяем) метод get. Вместо того, чтобы что-либо возвращать, мы предоставляем текст или JSON-сериализуемый объект для записи в тело ответа с помощью self.write. После этого мы позволяем RequestHandler взять на себя оставшуюся часть работы, которая должна быть выполнена перед отправкой ответа.

В настоящее время это представление не имеет никакой реальной связи с самим приложением Tornado. Мы должны вернуться в __init__.py и немного обновить основную функцию:

Подключение базы данных

Если мы хотим сохранить данные, нам нужно подключить базу данных. Мы будем использовать специфический для платформы вариант SQLAlchemy под названием tornado-sqlalchemy.

Зачем использовать это вместо простой SQLAlchemy? Что ж, tornado-sqlalchemy обладает всеми преимуществами простой SQLAlchemy, поэтому мы все еще можем объявлять модели с общей базой, а также использовать все типы данных столбцов и отношения, к которым мы привыкли. Помимо того, что мы уже знаем по привычке, tornado-sqlalchemy предоставляет доступный асинхронный шаблон для своей функциональности запросов к базе данных, специально предназначенной для работы с существующим циклом ввода-вывода Tornado.

Мы подготовили почву, добавив tornado-sqlalchemy и psycopg2 в setup.py в список необходимых пакетов. В models.py мы объявляем наши модели.

Нам все еще нужно подключить tornado-sqlalchemy к реальному приложению. В __init__.py мы будем определять базу данных и интегрировать ее в приложение.

Подобно фабрике сеансов, мы можем использовать make_session_factory, чтобы получить URL базы данных и создать объект, единственная цель которого – обеспечить соединения с базой данных для наших представлений. Затем мы связываем его с нашим приложением, передавая вновь созданную фабрику в объект Application с аргументом ключевого слова session_factory.

Наконец, инициализация и управление базой данных будут выглядеть так же, как и для Flask и Pyramid (то есть, отдельный сценарий управления БД, работа с базовым объектом и т. д.).

Наверх
Меню