Автор оригинала: Arun Ravindran.
В декабре 2019 года мы увидели выпуск Django 3.0 с интересной новой функцией – поддержкой серверов ASGI. Я был заинтригован тем, что это означало. Когда я проверил тесты производительности асинхронных веб-фреймворков Python, они были до смешного быстрее, чем их синхронные аналоги, часто в 3–5 раз.
Поэтому я решил протестировать производительность Django 3.0 с помощью очень простой настройки Docker. Результаты, хотя и не впечатляющие, все же были впечатляющими. Но перед этим вам может потребоваться немного информации об ASGI.
Это был 2003 год, и различные веб-фреймворки Python, такие как Zope , Quixote раньше поставлялись с собственными веб-серверами или имели собственные собственные интерфейсы для взаимодействия с популярными веб-серверами, такими как Apache.
Быть веб-разработчиком на Python означало искренне стремиться изучить весь стек, но при этом заново изучать все, если вам понадобится другая структура. Как вы понимаете, это привело к фрагментации. PEP 333 – «Интерфейс шлюза веб-сервера Python v1.0» попытался решить эту проблему, определив простой стандартный интерфейс, называемый WSGI (интерфейс шлюза веб-сервера). Его великолепие заключалось в простоте.
Фактически, вся спецификация WSGI может быть упрощена (без каких-либо сложных деталей), поскольку серверная сторона вызывает вызываемый объект (т.е. что угодно, от функции Python до класса с вызовом метод), предоставляемый платформой или приложением . Если у вас есть компонент, который может играть обе роли, значит, вы создали « промежуточное ПО » или промежуточный уровень в этом конвейере. Таким образом, компоненты WSGI можно легко объединить в цепочку для обработки запросов.
Когда подключение стало легким, последовало веселье
WSGI стал настолько популярным, что был принят не только крупными веб-фреймворками, такими как Django и Pylons , но также с помощью микрофреймворков, таких как Bottle . Ваш любимый фреймворк можно подключить к любому серверу приложений, совместимому с WSGI, и он будет работать безупречно. Это было настолько просто и интуитивно понятно, что не было никаких оправданий не использовать его.
Итак, если нас устраивает WSGI, зачем нам придумать ASGI? Ответ будет очевиден, если вы пошли по пути веб-запроса. Посмотрите мою анимацию передачи веб-запроса в Django . Обратите внимание, как фреймворк ожидает после запроса к базе данных перед отправкой ответа. Это недостаток синхронной обработки.
Откровенно говоря, этот недостаток не был очевиден или актуален до появления Node.js в 2009 году. Райана Даля, создателя Node.js, беспокоил Проблема C10K , например, почему популярные веб-серверы, такие как Apache, не могут обрабатывать 10 000 или более одновременных подключений (с учетом типичного аппаратного обеспечения веб-сервера у него может закончиться память). Он спросил : «Что делает программа, запрашивая базу данных?».
Похоже, она ждала вечно
Ответ, конечно же, был пуст. Он ждал ответа от базы данных. Райан утверждал, что веб-серверы вообще не должны ждать операций ввода-вывода. Вместо этого он должен переключиться на обслуживание других запросов и получить уведомление, когда медленное действие завершится. Используя эту технику, Node.js может обслуживать на много порядков больше пользователей, используя меньше памяти и в одном потоке!
Становилось все более очевидным, что асинхронные архитектуры, основанные на событиях, являются правильным способом решения многих видов проблем параллелизма. Вероятно, поэтому создатель Python Гвидо сам работал над поддержкой языкового уровня в проекте Tulip , который позже стал < модуль href = “https://docs.python.org/3/library/asyncio.html”> asyncio . Со временем в Python 3.7 были добавлены новые ключевые слова async
и await
для поддержки асинхронных циклов событий. Это имеет довольно важные последствия не только для написания кода Python, но и для его выполнения.
Хотя написание асинхронного кода на Python может показаться таким же простым, как вставка ключевого слова async перед определением функции, вы должны быть очень осторожны, чтобы не нарушить важное правило – Не смешивайте синхронный и асинхронный код .
Это связано с тем, что синхронный код может блокировать цикл событий в асинхронном коде. Такие ситуации могут остановить ваше приложение. Как пишет Эндрю Гудвин , код разбивается на два мира – «синхронный» и «асинхронный». с разными библиотеками и стилями вызова.
Когда два мира сталкиваются, результаты могут быть довольно неожиданными
Возвращаясь к WSGI, это означает, что мы не можем просто написать асинхронный вызываемый объект и подключить его. WSGI был написан для синхронного мира. Нам понадобится новый механизм для вызова асинхронного кода. Но если каждый напишет свои собственные механизмы, мы вернемся в ад несовместимости, с которого начали. Поэтому нам нужен новый стандарт, аналогичный WSGI для асинхронного кода. Так родился ASGI .
У ASGI были и другие цели. Но перед этим давайте посмотрим на два похожих веб-приложения, приветствующих «Hello World» в стиле WSGI и ASGI.
В WSGI:
def application(environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
return b"Hello, World"
В ASGI:
async def application(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": [(b"Content-Type", "text/plain")]})
await send({"type": "http.response.body", "body": b"Hello World"})
Обратите внимание на изменение аргументов, переданных в вызываемые объекты. Аргумент scope
аналогичен более раннему аргументу Environment
. Аргумент send
соответствует start_response
. Но аргумент receive
новый. Это позволяет клиентам небрежно передавать сообщения на сервер в протоколах, таких как WebSockets, которые позволяют двунаправленную связь.
Как и WSGI, вызываемые объекты ASGI могут быть связаны друг за другом для обработки веб-запросов (а также запросов других протоколов). Фактически, ASGI является надмножеством WSGI и может вызывать вызываемые объекты WSGI. ASGI также поддерживает длительный опрос, медленную потоковую передачу и другие захватывающие типы ответов без дополнительной загрузки, что приводит к более быстрым ответам.
Таким образом, ASGI представляет новые способы создания асинхронных веб-интерфейсов и обработки двунаправленных протоколов. Ни клиенту, ни серверу не нужно ждать обмена данными друг с другом – это может произойти в любое время асинхронно. Существующие веб-фреймворки на основе WSGI, написанные в синхронном коде, не поддерживают этот управляемый событиями способ работы.
Джанго развивается
Это также подводит нас к сути проблемы, связанной с внедрением в Django всех преимуществ асинхронности – весь Django был написан в коде синхронного стиля. Если нам нужно написать какой-либо асинхронный код, тогда должен быть клон всего фреймворка Django, написанный в асинхронном стиле. Другими словами, создайте два мира Django.
Что ж, не паникуйте – нам, возможно, не придется писать весь клон, поскольку есть умные способы повторно использовать фрагменты кода между двумя мирами. Но, как справедливо отмечает Эндрю Годвин, возглавляющий Async Project Django, «это одна из самых значительных переделок Django в его истории». Амбициозный проект, включающий повторную реализацию таких компонентов, как ORM, обработчик запросов, средство визуализации шаблонов и т. Д. В асинхронном стиле. Это будет выполняться поэтапно и в несколько выпусков. Вот как Эндрю представляет это (не следует воспринимать как установленный график):
- Django 3.0 – Сервер ASGI
- Django 3.1 – Асинхронные представления (см. Пример ниже)
- Django 3.2/4.0 – Асинхронный ORM
Вы можете подумать, что насчет остальных компонентов, таких как рендеринг шаблонов, формы, кэш и т.д. Но это ключевые вехи в развитии Django для работы в асинхронном мире.
Это подводит нас к первому этапу.
Джанго говорит об ASGI
В версии 3.0 Django может работать в режиме «асинхронный снаружи, синхронизирующийся внутри». Это позволяет ему общаться со всеми известными серверами ASGI, такими как:
- Daphne – эталонный сервер ASGI, написанный на Twisted
- Uvicorn – быстрый ASGI-сервер на основе uvloop и httptools
- Hypercorn – сервер ASGI на основе библиотек sans-io hyper, h11, h2 и wsproto.
Важно повторить, что внутри Django все еще синхронно обрабатывает запросы в пуле потоков. Но базовый сервер ASGI будет обрабатывать запросы асинхронно.
Это означает, что ваши существующие проекты Django не требуют изменений. Думайте об этом изменении как о новом интерфейсе, через который HTTP-запросы могут поступать в ваше приложение Django.
Но это важный первый шаг в преобразовании Django из внешнего во внутрь. Вы также можете начать использовать Django на сервере ASGI, который обычно работает быстрее.
Как пользоваться ASGI?
Каждый проект Django (начиная с версии 1.4 ) поставляется с файлом wsgi.py , который является модулем обработчика WSGI. При развертывании в производственной среде вы укажете свой сервер WSGI, как gunicorn, на этот файл. Например, вы могли видеть эту строку в своем файле Docker compose .
command: gunicorn mysite.wsgi:application
Если вы создадите новый проект Django (например, созданный с помощью команды django-admin startproject
), вы найдете новый файл asgi.py вместе с wsgi.py. Вам нужно будет указать свой ASGI-сервер (например, дафен) на этот файл-обработчик ASGI. Например, указанная выше строка будет изменена на:
command: daphene mysite.asgi:application
Обратите внимание, что для этого требуется наличие файла asgi.py .
Запуск существующих проектов Django под ASGI
Ни один из проектов, созданных до Django 3.0, не имеет asgi.py. Итак, как вы подойдете к его созданию? Это очень просто.
Вот параллельное сравнение (строки документации и комментарии) wsgi.py и asgi.py для проекта Django:
WSGI
ASGI
import osfrom django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()
import osfrom django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_asgi_application()
Если вы слишком прищуриваетесь, чтобы найти различия, позвольте мне помочь – везде «wsgi» заменяется на «asgi». Да, это так же просто, как взять существующий файл wsgi.py и запустить замену строки s/wsgi/asgi/g
.
Вам следует позаботиться о том, чтобы не вызывать какой-либо код синхронизации в обработчике ASGI в вашем asgi.py. Например, если вы по какой-то причине вызываете некоторый веб-API в обработчике ASGI, то он должен быть вызываемым asyncio.
ASGI против производительности WSGI
Я провел очень простой тест производительности, опробовав проект опросов Django в конфигурациях ASGI и WSGI. Как и во всех тестах производительности, вы должны воспринимать мои результаты с большим количеством соли. Моя установка Docker включает Nginx и Postgresql. Фактическое нагрузочное тестирование проводилось с использованием универсального инструмента Locust .
Тестовый пример открывал форму опроса в приложении опросов Django и отправлял случайное голосование. Он выполняет n запросов в секунду, когда есть n пользователей. Время ожидания составляет от 1 до 2 секунд.
Быть быстрым недостаточно, нужно избегать неудач
Результаты, показанные ниже, показывают увеличение количества одновременных пользователей примерно на 50% при работе в режиме ASGI по сравнению с режимом WSGI.
Пользователи
100
200
300
400
500
600
700
Ошибки WSGI
0%
0%
0%
5%
12%
35%
50%
Ошибки ASGI
0%
0%
0%
0%
0%
15%
20%
По мере увеличения количества одновременных запросов обработчик WSGI или ASGI не сможет справиться сверх определенной точки, что приведет к ошибкам или сбоям. Количество запросов в секунду после начала сбоев WSGI сильно различается. Работа ASGI намного стабильнее даже после сбоев.
Как видно из таблицы, количество одновременных пользователей на моем компьютере составляет около 300 для WSGI и 500 для ASGI. Это примерно на 66% больше числа пользователей, которых серверы могут обрабатывать без ошибок. Ваш пробег может отличаться.
Частые вопросы
Недавно я выступал с докладом об ASGI и Django на BangPypers , и у аудитории было много интересных вопросов (даже после мероприятие). Я подумал, что обращусь к ним здесь (в произвольном порядке):
Channels был создан для поддержки асинхронных протоколов, таких как Websockets и HTTP с длинным опросом. Приложения Django по-прежнему работают синхронно . Каналы – это официальный проект Django, но не часть ядра Django.
Проект Django Async будет поддерживать написание приложений Django с асинхронным кодом в дополнение к синхронному. Async – это часть ядра Django.
Обеими руководил Эндрю Гудвин.
В большинстве случаев это самостоятельные проекты. У вас может быть проект, в котором используется одно или оба. Например, если вам нужно поддерживать приложение чата через веб-сокеты, вы можете использовать каналы без использования интерфейса ASGI Django. С другой стороны, если вы хотите создать асинхронную функцию в представлении Django, вам придется дождаться поддержки Django Async для представлений.
При установке только Django 3.0 в вашу среду будет установлено следующее:
$ pip freeze
Библиотека asgiref – это новая зависимость. Он содержит оболочки функций sync-to-async и async-to-sync, так что вы можете вызывать код синхронизации из async и наоборот. Он также содержит StatelessServer и адаптер WSGI-ASGI.
Версия 3.0 может показаться большим изменением по сравнению с предыдущей версией Django 2.2. Но это немного вводит в заблуждение. Проект Django не следует семантической версии в точности (где изменение основного номера версии может нарушить работу API), и различия объясняются в Release Process .
Вы заметите очень мало серьезных обратных несовместимых изменений в Django 3.0 примечания к выпуску . Если в вашем проекте они не используются, вы можете обновить его без каких-либо модификаций.
Тогда почему номер версии подскочил с 2.2 до 3.0? Это объясняется в разделе периодичность выпуска :
Начиная с Django 2.0, номера версий будут использовать свободную форму семантического управления версиями , так что каждая версия, следующая за LTS, будет переходить к следующей «нулевой точке». версия. Например: 2.0, 2.1, 2.2 (LTS), 3.0, 3.1, 3.2 (LTS) и т. Д.
Поскольку последний выпуск Django 2.2 был выпуском с долгосрочной поддержкой (LTS), в следующем выпуске основной номер версии должен был быть увеличен до 3.0. Вот и все!
Да. Асинхронное программирование можно рассматривать как совершенно необязательный способ написания кода на Django. Знакомый синхронный способ использования Django продолжит работать и будет поддерживаться.
Эндрю пишет :
Даже если существует полностью асинхронный путь через обработчик, также необходимо поддерживать совместимость с WSGI; для этого WSGIHandler будет сосуществовать вместе с новым ASGIHandler и запускать систему внутри одноразовой петли событий, сохраняя ее синхронной внешне и асинхронной внутренне.
Это позволит асинхронным представлениям выполнять несколько асинхронных запросов и запускать недолговечные сопрограммы даже внутри WSGI, если вы решите работать таким образом. Однако, если вы решите работать под ASGI, вы также получите преимущества того, что запросы не блокируют друг друга и используют меньше потоков.
Как объяснялось ранее в дорожной карте Async Project, ожидается, что Django 3.1 представит асинхронные представления, которые будут поддерживать написание асинхронного кода, например:
async def view(request):
await asyncio.sleep(0.5)
return HttpResponse("Hello, async world!")
Вы можете сколько угодно смешивать асинхронные и синхронизирующие представления, промежуточное ПО и тесты; Django гарантирует, что у вас всегда будет правильный контекст выполнения. Звучит круто, правда?
На момент написания этот патч почти наверняка появится в 3.1, ожидая окончательного обзора.
Заключение
Мы рассмотрели много предыстории о том, что привело к тому, что Django поддерживает асинхронные возможности. Мы попытались понять ASGI и его сравнение с WSGI. Мы также обнаружили некоторые улучшения производительности с точки зрения увеличения количества одновременных запросов в режиме ASGI. Также был рассмотрен ряд часто задаваемых вопросов, связанных с поддержкой ASGI со стороны Django.
Я считаю, что поддержка асинхронности в Django может изменить правила игры. Это будет одна из первых крупных веб-фреймворков Python, которая перерастет в обработку асинхронных запросов. Замечательно, что это сделано с большой осторожностью, чтобы не нарушить обратную совместимость.
Обычно я не делаю свои технические прогнозы публичными (честно говоря, многие из них подтвердились годами). Итак, вот мой технический прогноз – Большинство развертываний Django будут использовать асинхронные функции через пять лет .
Этого должно быть достаточно, чтобы проверить это!
Спасибо Эндрю Гудвину за комментарии к раннему черновику. Все иллюстрации любезно предоставлены Иллюстрации старых книг