[Из песочницы] Введение в ASGI: становление асинхронной веб-экосистемы Python
Привет, Хабр! Представляю вашему вниманию перевод статьи «Introduction to ASGI: Emergence of an Async Python Web Ecosystem» автора Florimond Manca.
«Черепахи рядом с водоемом», Ricard Baraham на unsplash.com
Python не замыкается только на Data Science, веб-разработка на Python вернулась с новым асинхронным витком в развитии языка!
Сейчас происходит много важных событий в экосистеме веб-разработки на Python. Одним из основных драйверов этих изменений является ASGI — Asynchronous Standard Gateway Interface.
Я уже несколько раз упоминал ASGI в моем блоге, в частности, когда анонсировал Bocadillo (асинхронный open-source веб-фреймворк на Python — прим.пер.) и tartiflette-starlette (библиотека для построения GraphQL API поверх HTTP через ASGI — прим.пер.), но я никогда не писал подробное введение о нем. Теперь я это сделаю.
Эта статья нацелена на людей, интересующихся последними трендами в веб-разработке на Python. Я хочу пригласить вас на экскурсию, из которой вы узнаете, что такое ASGI, и что он означает для современной веб-разработки в мире Python.
Прежде чем мы начнем, я хотел бы рассказать, что недавно создал awesome-asgi — отличный список для отслеживания постоянно расширяющейся экосистемы ASGI.
Все началось с async/await
В отличие от JavaScript или Go, в момент появления Python не предоставлял возможность асинхронного исполнения кода. Долгое время параллельное выполнение кода в Python могло быть реализовано только с помощью многопоточной или многопроцессорной обработки, либо с использованием специализированных сетевых библиотек, таких как eventlet, gevent или Twisted. (Еще в 2008 году у Twisted был API для асинхронных корутин, например, в виде inlineCallbacks
и deferredGenerator
)
Все изменилось в Python 3.4+. В Python 3.4 в стандартную библиотеку включили asyncio, в результате появилась поддержка кооперативной многозадачности на основе генераторов и синтаксиса yield from
.
Позже в Python 3.5 добавлен синтаксис async/await
. Благодаря этому появились нативные корутины, независящие от лежащей в основе реализации, что привело к золотой лихорадке вокруг параллелизма в Python.
Началась сумасшедшая гонка! С момента выпуска версии 3.5 сообщество буквально асинхронизовывает все вокруг. Если вам интересно, многие из получившихся проектов перечислены в aio-libs и awesome-asyncio.
Как вы догадались, это также означает, что веб-серверы и приложения на Python движутся в сторону асинхронности. На самом деле, все крутые ребята делают это! (Даже Django) (Habr: Django 3.0 будет асинхронным, уже вышла 02.12.2019 — прим.пер.)
Обзор ASGI
Итак, как же ASGI вписывается во все это?
Верхнеуровнево ASGI можно рассматривать как связующее звено, которое позволяет асинхронным Python серверам и приложениям взаимодействовать друг с другом. Он повторяет множество архитектурных идей из WSGI, и зачастую представляется как его преемник со встроенной асинхронностью.
Вот так его можно изобразить на диаграмме:
На очень высоком уровне ASGI — это интерфейс для коммуникации между приложениями и серверами. Но на самом деле, все немного сложнее.
Чтобы разобраться, как ASGI действительно работает, давайте взглянем на спецификацию ASGI.
ASGI состоит из двух различных компонентов:
- Сервера протокола (protocol server) — слушает сокеты и преобразует их в соединения и сообщения о событиях внутри каждого соединения.
- Приложения (application), которое живет внутри сервера протокола, его экземпляр создается один раз для каждого соединения и обрабатывает сообщения о событиях по мере их возникновения.
Таким образом, согласно спецификации, то, что действительно указывает ASGI — это формат сообщения и то, как эти сообщения должны передаваться между приложением и сервером протокола, который его запускает.
Теперь мы можем составить более детальную версию диаграммы:
В протоколе есть еще много более интересных деталей. Например, вы можете взглянуть на спецификацию HTTP и WebSocket.
Кроме того, хотя спецификация сильно фокусируется на взаимодействии между сервером и приложением, ASGI удается охватить гораздо больше аспектов.
Мы доберемся до этого через минуту, но сначала…
Основы ASGI
Теперь, когда мы увидели, как ASGI вписывается в веб-экосистему Python, давайте более подробно рассмотрим, как это воплощается в коде.
ASGI опирается на простую модель: когда клиент подключается к серверу, создается экземпляр приложения. Затем входящие данные передаются в приложение и отправляются обратно все данные, которые оно возвращает.
Передача данных в приложение здесь в действительности означает вызов приложения, как если бы оно было функцией, т.е. чем-то, что принимает некоторые входные данные и возвращает выходные.
На самом деле, все, что представляет собой ASGI-приложение — это callable (вызываемый объект). Параметры этого вызываемого объекта, опять же, определяются спецификацией ASGI:
async def app(scope, receive, send):
...
Сигнатура этой функции — это как раз то, что означает «I» в «ASGI»: интерфейс, который должно реализовать приложение, чтобы сервер смог его вызвать.
Давайте рассмотрим параметры функции:
scope
— это словарь, содержащий информацию о входящем запросе. Его содержимое отличается для HTTP и WebSocket соединений.receive
— асинхронная функция для получения сообщений о событиях ASGI.send
— асинхронная функция для отправки сообщений о событиях ASGI.
По сути, эти параметры позволяют получать (receive()
) и передавать (send()
) данные по каналу связи, который поддерживает сервер протокола, а также понимать, в каком контексте (или scope
) этот канал был создан.
Не знаю как вам, но мне очень нравятся общий вид и структура этого интерфейса. В любом случае, сейчас посмотрим на пример кода.
Покажите код!
Чтобы получить практическое представление о том, как выглядит ASGI, я создал минимальный проект на голом ASGI, который демонстрирует HTTP-приложение, обслуживаемое uvicorn (популярный ASGI-сервер):
async def app(scope, receive, send):
assert scope["type"] == "http"
await send({
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
]
})
await send({
"type": "http.response.body",
"body": b"Hello, world!",
})
Исходный код — https://glitch.com/edit/#!/asgi-hello-world
Здесь мы используем send()
для отправки HTTP-ответа клиенту: сначала отправляем заголовки, а затем тело ответа.
Я признаю, что из-за всех этих словарей и необработанных бинарных данных голый ASGI не очень удобен для работы.
К счастью, есть варианты более высокого уровня — и именно тогда я начинаю говорить о Starlette.
Starlette — поистине фантастический проект, и, по-моему, фундаментальная часть экосистемы ASGI.
Кратко, он предоставляет набор высокоуровневых компонентов, таких как запросы и ответы, которые можно использовать, чтобы абстрагироваться от некоторых деталей ASGI. Вот, взгляните на «hello world» в Starlette:
# app.py
from starlette.responses import PlainTextResponse
async def app(scope, receive, send):
assert scope["type"] == "http"
response = PlainTextResponse("Hello, world!")
await response(scope, receive, send)
В Starlette есть все, что вы ожидаете от настоящего веб-фреймворка — routing, middleware и т.д. Но я решил показать эту урезанную версию, чтобы намекнуть на реальную силу ASGI, которая является…
Черепахи на всем пути
Интересная и меняющая правила игры концепция ASGI — это «Черепахи на всем пути», выражение, которое первоначально придумал (я думаю?) Andrew Godwin, создавший Django Migrations и сейчас занимающийся переработкой Django для поддержки асинхронности.
Но что именно это означает?
Поскольку ASGI это абстракция, которая позволяет сказать, в каком контексте мы находимся, и получать и отправлять данные в любое время, то есть идея, что ASGI можно использовать не только между серверами и приложениями, но и действительно в любой точке стека.
Например, объект Starlette Response
это само приложение ASGI. На самом деле, мы можем сократить код в примере приложения выше до такого:
# app.py
app = PlainTextResponse("Hello, world!")
Насколько нелепо это выглядит?!
Но подождите, это еще не все.
Более глубокое следствие «черепах на всем пути» заключается в том, что мы можем создавать всевозможные приложения, middleware, библиотеки и другие проекты и гарантировать, что они будут совместимыми до тех пор пока они все реализуют интерфейс приложения ASGI.
(К тому же, из моего личного опыта построения Bocadillo, прием интерфейса ASGI очень часто (если не всегда) приводит к гораздо более чистому коду)
Например, мы можем создать ASGI middleware (т.е. приложение, которое обертывает другое приложение), чтобы отобразить время, за которое был выполнен запрос:
# app.py
import time
class TimingMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
start_time = time.time()
await self.app(scope, receive, send)
end_time = time.time()
print(f"Took {end_time - start_time:.2f} seconds")
Чтобы использовать его, мы просто оборачиваем им приложение…
# app.py
import asyncio
from starlette.responses import PlainTextResponse
async def app(scope, receive, send):
await asyncio.sleep(1)
response = PlainTextResponse("Hello, world!")
await response(scope, receive, send)
app = TimingMiddleware(app)
… и это будет волшебным образом просто работать.
$ uvicorn app:app
INFO: Started server process [59405]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
...
INFO: ('127.0.0.1', 62718) - "GET / HTTP/1.1" 200
Took 1.00 seconds
Восхитительно, что в TimingMiddleware
можно обернуть любое ASGI-приложение. Внутреннее приложение в этом примере супер-простое, но это может быть полноценный, реальный проект (представьте сотни API и WebSocket endpoint-ов) — это не имеет значения, пока интерфейс совместим с ASGI.
(Есть версия этого middleware более подготовленная к промышленному использованию: timing-asgi.)
Почему это должно волновать?
Хотя я думаю, что совместимость является очень сильным аргументом, есть еще много преимуществ в использовании компонентов на основе ASGI для построения веб-приложений Python.
- Скорость: асинхронная природа ASGI приложений и серверов делает их действительно быстрыми (по крайней мере, для Python) — мы говорим о 60k-70k запросов в секунду (полагая, что Flask и Django достигают только 10–20k в аналогичной ситуации).
- Возможности: серверы и платформы ASGI предоставляют вам доступ к по существу параллельным функциям (WebSocket, Server-Sent Events, HTTP/2), которые невозможно реализовать с помощью синхронного кода и WSGI.
- Стабильность: ASGI как спецификация существует уже около 3 лет, и версия 3.0 считается очень стабильной. В результате этого основные части экосистемы стабилизируются.
С точки зрения библиотек и инструментов, не думаю, что мы можем сказать, что добрались до необходимого уровня. Но благодаря очень активному сообществу, у меня есть большие надежды, что экосистема ASGI очень скоро достигнет паритета функций с традиционной синхронной/WSGI экосистемой.
Где можно найти компоненты, совместимые с ASGI?
На самом деле, все больше и больше людей строят и улучшают проекты на основе ASGI. Очевидно, это серверы и веб-фреймворки, но также есть middleware и ориентированные на продукт приложения, такие как Datasette.
Ниже несколько примеров компонентов, не являющихся веб-феймворками, которые меня интересуют:
- Mangum: поддержка ASGI для AWS Lambda.
- datasette-auth-github: аутентификация GitHub для ASGI-приложений.
- tartiflette-starlette (я написал его!): Поддержка ASGI для Tartiflette, асинхронного движка GraphQL.
Восхитительно наблюдать, что экосистема успешно развивается, однако, мне лично было трудно не отставать от изменений.
Именно поэтому я создал awesome-asgi. Я надеюсь, что это поможет всем идти в ногу со всеми удивительными вещами, которые происходят в мире ASGI. (И видя, что он почти достиг 100 звезд за несколько дней, у меня есть ощущение, что действительно была необходимость собрать в одном месте информацию о ресурсах про ASGI.)
Выводы
Хотя это может выглядеть как подробности реализации, я уверен, что ASGI заложил основы для новой эры в веб-разработке на Python.
Если вы хотите узнать больше о ASGI, ознакомьтесь с различными публикациями (статьями и выступлениями), перечисленными в awesome-asgi
. Если хотите потрогать руками, попробуйте любой из следующих проектов:
- uvicorn: сервер ASGI.
- Starlette: ASGI фреймворк.
- TypeSystem: валидация данных и рендеринг форм.
- Databases: библиотека асинхронных баз данных.
- orm: асинхронный ORM.
- HTTPX: асинхронный HTTP client с поддержкой вызова ASGI-приложений (полезен как клиент для тестирования).
Эти проекты были созданы и поддерживаются компанией Encode, в основном Томом Кристи (Tom Christie). Есть открытые обсуждения по созданию команды поддержки Encode, так что если вы искали возможность поучаствовать в разработке open-source, то у вас есть такая возможность!
Получайте удовольствие от путешествия в мир ASGI!