Руководство по ASGI в Django 3.0 и его производительности

В декабре 2019 года вышел релиз Django 3.0 с интересной новой возможностью - поддержкой ASGI-серверов. Я был заинтригован тем, что это означает. Когда я проверял бенчмарки производительности асинхронных веб-фреймворков Python, они были до смешного быстрее своих синхронных аналогов, часто в 3x-5x раз.

Так что я решил проверить производительность Django 3.0 с помощью очень простой установки Docker. Результаты - хотя и не впечатляющие - все же были впечатляющими. Но перед этим вам, возможно, понадобится немного информации об ASGI.

До ASGI был WSGI

Это был 2003 год, и различные веб-фреймворки Python, такие как Zope, Quixote, поставлялись с собственными веб-серверами или имели собственные интерфейсы для взаимодействия с популярными веб-серверами, такими как Apache.

Быть веб-разработчиком на Python означало усердно изучать весь стек, но переучиваться всему, если вам нужна была другая платформа. Как вы понимаете, это привело к фрагментации. PEP 333 — “Интерфейс шлюза веб-сервера Python v1.0” попытался решить эту проблему, определив простой стандартный интерфейс под названием WSGI (Интерфейс шлюза веб-сервера). Его великолепие заключалось в простоте.

На самом деле, всю спецификацию WSGI можно упростить (для удобства опустив некоторые сложные детали), поскольку серверная сторона вызывает вызываемый объект (то есть что угодно, от функции Python до класса с call), предоставляемый платформой или приложением. Если у вас есть компонент, который может играть обе роли, значит, вы создали “промежуточное ПО” или промежуточный слой в этом конвейере. Таким образом, компоненты WSGI можно легко связать вместе для обработки запросов.

Illustration

When connecting became easy, merriment followed

WSGI стал настолько популярным, что его приняли не только крупные веб-фреймворки, такие как Django и Pylons, а также с помощью микрофреймворков, таких как Bottle. Ваш любимый фреймворк можно подключить к любому WSGI-совместимому серверу приложений, и он будет работать безупречно. Это было настолько просто и интуитивно понятно, что действительно не было оправдания не использовать его.

Дорожные "блоки" в масштабе

Если нас вполне устраивал WSGI, зачем нам было придумывать ASGI? Ответ будет очевиден, если вы прошли путь веб-запроса. Посмотрите мою анимацию того, как веб-запрос передается в Django. Обратите внимание, как платформа ожидает после запроса к базе данных перед отправкой ответа. Это недостаток синхронной обработки.

Откровенно говоря, этот недостаток не был очевиден и не проявлялся до тех пор, пока в 2009 году не появился Node.js. Создателя Node.js Райана Даля беспокоил Проблема C10K, то есть почему популярные веб-серверы, такие как Apache, не могут обрабатывать 10 000 или более одновременных подключений (учитывая типичное оборудование веб-сервера, ему не хватило бы памяти) . Он задал вопрос “Что делает программное обеспечение во время запроса к базе данных?".

Illustration

Looks like she has been waiting forever

Ответ, конечно же, был отрицательным. Он ждал ответа от базы данных. Райан утверждал, что веб-серверы вообще не должны ждать операций ввода-вывода. Вместо этого он должен переключиться на обслуживание других запросов и получить уведомление, когда медленная деятельность будет завершена. Используя эту технику, Node.js мог бы обслуживать на порядки больше пользователей, используя меньше памяти и в один поток!

Становилось все более очевидным, что асинхронные архитектуры, основанные на событиях, являются правильным способом решения многих проблем параллелизма. Вероятно, именно поэтому Гвидо, создатель Python, сам работал над поддержкой языкового уровня в проекте Tulip, который позже стал модуль asyncio. В итоге в Python 3.7 добавлены новые ключевые слова async и await для поддержки асинхронных циклов событий. Это имеет довольно серьезные последствия не только для написания кода Python, но и для его выполнения.

Два мира Python

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

Это связано с тем, что синхронный код может блокировать цикл обработки событий в асинхронном коде. Такие ситуации могут поставить ваше приложение в тупик. Как пишет Эндрю Гудвин, код разделяется на два мира: “Синхронный” и “Асинхронный” с разными библиотеками и стилями вызова.

Illustration

Когда сталкиваются два мира, результаты могут быть весьма неожиданными

Возвращаясь к 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"

В АГСИ:


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 подобен предыдущему аргументу environ. Аргумент send соответствует start_response. Но аргумент receive новый. Это позволяет клиентам небрежно передавать сообщения на сервер по таким протоколам, как WebSockets, которые обеспечивают двустороннюю связь.

Как и в WSGI, вызываемые модули ASGI могут быть соединены один за другим для обработки веб-запросов (а также других протокольных запросов). Фактически, ASGI является надмножеством WSGI и может вызывать вызываемые модули WSGI. ASGI также поддерживает длинные опросы, медленные потоки и другие захватывающие типы ответов без побочной нагрузки, что приводит к более быстрым ответам.

Таким образом, ASGI представляет новые способы построения асинхронных веб-интерфейсов и работы с двунаправленными протоколами. Ни клиенту, ни серверу не нужно ждать друг друга для обмена данными - это может произойти в любой момент асинхронно. Существующие веб-фреймворки на базе WSGI, написанные на синхронном коде, не будут поддерживать такой событийно-ориентированный способ работы.

Django Evolves

Это также подводит нас к сути проблемы переноса всего асинхронного добра в Django - весь Django был написан в синхронном стиле. Если нам нужно написать какой-либо асинхронный код, то необходимо создать клон всего фреймворка Django, написанный в асинхронном стиле. Другими словами, создайте два мира Django.

Ну, не паникуйте — нам, возможно, не придется писать весь клон, поскольку есть умные способы повторного использования фрагментов кода между двумя мирами. Но, как справедливо отмечает Эндрю Годвин, возглавляющий асинхронный проект Django, «это одна из крупнейших переработок Django в мире». его история». Амбициозный проект, включающий повторную реализацию таких компонентов, как ORM, обработчик запросов, средство визуализации шаблонов и т. д., в асинхронном стиле. Это будет сделано поэтапно и в несколько релизов. Вот как Эндрю представляет это (не следует воспринимать как согласованный график):

  • Django 3.0 - ASGI Server
  • Django 3.1 - Async Views (смотрите example below)
  • Django 3.2/4.0 - Async ORM

Возможно, вы подумаете, а как насчет остальных компонентов, таких как рендеринг шаблонов, формы, кэш и т.д.? Они могут оставаться синхронными или асинхронная реализация будет включена в будущую дорожную карту. Но вышеперечисленное - это ключевые вехи в развитии Django для работы в асинхронном мире.

Это подводит нас к первой фазе.

Django рассказывает об ASGI

В версии 3.0 Django может работать в режиме "async outside, sync inside". Это позволяет ему общаться со всеми известными 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 (например, daphene) на этот файл обработчика ASGI. Например, приведенная выше строка будет изменена на:


command: daphene mysite.asgi:application

Обратите внимание, что для этого необходимо наличие файла asgi.py.

Запуск существующих проектов Django под ASGI

.

Ни один из проектов, созданных до Django 3.0, не имеет asgi.py. Как же его создать? Это довольно просто.

Здесь представлено боковое сравнение (документация и комментарии опущены) wsgi.py и asgi.py для проекта Django:

WSGI ASGI

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault(‘DJANGO_SETTINGS_MODULE’, ‘mysite.settings’)

application = get_wsgi_application()

 


import os

from 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 Performance

Я провел очень простой тест производительности, протестировав проект опросов Django в конфигурациях ASGI и WSGI. Как и все тесты производительности, вы должны относиться к моим результатам с изрядной дозой соли. Моя установка Docker включает Nginx и Postgresql. Фактическое нагрузочное тестирование проводилось с использованием универсального инструмента Locust.

Тестовым примером было открытие формы опроса в приложении опросов Django и отправка случайного голосования. Он выполняет n запросов в секунду при наличии n пользователей. Время ожидания составляет от 1 до 2 секунд.

Illustration

Быть быстрым недостаточно, нужно избегать сбоев

Приведенные ниже результаты свидетельствуют об увеличении количества одновременных пользователей примерно на 50% при работе в режиме ASGI по сравнению с режимом WSGI.

Users 100 200 300 400 500 600 700
WSGI Failures 0% 0% 0% 5% 12% 35% 50%
ASGI Failures 0% 0% 0% 0% 0% 15% 20%

По мере увеличения количества одновременных запросов обработчик WSGI или ASGI не сможет справиться с ними до определенного момента, что приведет к ошибкам или сбоям. Количество запросов в секунду после начала сбоев WSGI варьируется в широких пределах. Производительность ASGI намного стабильнее даже после сбоев.

Как видно из таблицы, количество одновременных пользователей составляет около 300 для WSGI и 500 для ASGI на моей машине. Это примерно 66% увеличение количества пользователей, с которыми серверы могут работать без ошибок. Ваш пробег может варьироваться.

Частые вопросы

Недавно я выступал с докладом об ASGI и Django на BangPypers, и у аудитории возникло много интересных вопросов (даже после мероприятия). Поэтому я решил ответить на них здесь (без особого порядка):

Вопрос. Является ли Django Async тем же самым, что и Channels?

Каналы были созданы для поддержки асинхронных протоколов, таких как веб-сокеты и HTTP с длительным опросом. Приложения Django по-прежнему работают синхронно. Каналы — это официальный проект Django, но он не является частью ядра Django.

Проект Django Async будет поддерживать написание приложений Django с асинхронным кодом в дополнение к синхронному. Async является частью ядра Django.

Обоими руководил Эндрю Гудвин.

В большинстве случаев это независимые проекты. Вы можете иметь проект, который использует один из них или оба. Например, если вам нужно поддерживать чат-приложение через веб-сокеты, то вы можете использовать Channels без использования ASGI-интерфейса Django. С другой стороны, если вы хотите сделать асинхронную функцию в представлении Django, то вам придется подождать поддержки Django Async для представлений.

Q. Какие-нибудь новые зависимости в Django 3.0?

Установка только Django 3.0 установит в вашу среду следующее:


$ pip freeze
asgiref==3.2.3
Django==3.0.2
pytz==2019.3
sqlparse==0.3.0

Библиотека asgiref - это новая зависимость. Она содержит обертки функций sync-to-async и async-to-sync, чтобы вы могли вызывать код sync из async и наоборот. Она также содержит StatelessServer и адаптер WSGI-to-ASGI.

Вопрос. Сломает ли обновление до Django 3.0 мой проект?

Версия 3.0 может показаться большим отличием от предыдущей версии Django 2.2. Но это немного вводит в заблуждение. Проект Django не следует точно семантической версии (где изменение основного номера версии может нарушить работу API), и различия объясняются в Процесс выпуска.

Вы заметите очень мало серьезных несовместимых назад изменений в 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. Вот, собственно, и все!

Вопрос. Могу ли я продолжать использовать WSGI?

Да. Асинхронное программирование можно рассматривать как совершенно необязательный способ написания кода в Django. Привычный синхронный способ использования Django будет продолжать работать и поддерживаться.

Эндрю пишет:

Даже при наличии полностью асинхронного пути через обработчик необходимо сохранить совместимость с WSGI; для этого WSGIHandler будет сосуществовать с новым ASGIHandler и запускать систему внутри однократного eventloop, сохраняя ее синхронной снаружи и асинхронной внутри.

Это позволит асинхронным представлениям выполнять несколько асинхронных запросов и запускать короткоживущие корутины даже внутри WSGI, если вы решите работать таким образом. Однако если вы решите работать под ASGI, вы также получите преимущества от того, что запросы не блокируют друг друга и используют меньше потоков

.

Вопрос. Когда я могу писать асинхронный код в Django?

Как объяснялось ранее в дорожной карте Async Project, ожидается, что в Django 3.1 будут представлены асинхронные представления, которые будут поддерживать написание асинхронного кода, например:


async def view(request):
	await asyncio.sleep(0.5)
    return HttpResponse("Hello, async world!")

Вы можете смешивать async и sync представления, промежуточное ПО и тесты сколько угодно; Django гарантирует, что в итоге вы всегда получите правильный контекст выполнения. Звучит довольно круто, не так ли?

На момент написания статьи этот патч почти наверняка попадет в 3.1, ожидая окончательного пересмотра.

Завершение

Мы много рассказывали о том, что привело Django к поддержке асинхронных возможностей. Мы попытались понять ASGI и его сравнение с WSGI. Мы также обнаружили некоторые улучшения производительности в плане увеличения количества одновременных запросов в режиме ASGI. Также был рассмотрен ряд часто задаваемых вопросов, связанных с поддержкой ASGI в Django.

Я считаю, что поддержка асинхронных запросов в Django может стать переломным моментом в игре. Это будет один из первых крупных веб-фреймворков Python, который перейдет к обработке асинхронных запросов. Восхищает то, что это делается с большой осторожностью, чтобы не нарушить обратную совместимость.

Обычно я не публикую свои технические прогнозы (откровенно говоря, многие из них подтвердились годами). Итак, вот мой технический прогноз — В большинстве развертываний Django через пять лет будут использоваться асинхронные функции.

Это должно быть достаточной мотивацией, чтобы проверить его!

Вернуться на верх