О том, как в питоне без протокола и задеплоить приложение не могут. Смешиваем ASGI с WSGI — вредные советы

от автора

Как должны выглядеть современные сервисы на питоне, многие имеют представление. Все они так или иначе имеют поддержку асинхронных операций. А вот как их лучше деплоить? Здесь некоторые руководства (как FastAPI) отвели целый раздел для рекомендаций, а некоторые (как Django) ограничились несколькими абзацами с крайне размытыми формулировками. Мне не посчастливилось следовать именно последнему.

Прочитав эту статью, Вы, возможно, захотите внести изменения в докерфайлы Ваших сервисов. Благодаря протоколам WSGI и ASGI, это можно сделать без особого труда. Именно поэтому все изложенные в статье советы — вредные. Также Вы узнаете о nginx unit — ещё об одном годном сервере приложений.

Немного предыстории. На одном проекте у нас с командой в очередной раз встала задача деплоя питоновского веб-приложения. Обычно выбор стоит между gunicorn и uwsgi. Но в этот раз я успел услышать про новый сервер nginx unit и очень хотел его попробовать. Предварительно, конечно, нужно было отговорить команду от всех адекватных вариантов.

Например, возьмём gunicorn — традиционный вариант, асинхронные фреймворки часто рекомендуют именно его. Для него есть асинхронный воркер, наряду с синхронным. Однако, если мы немного почитаем документацию, нас будет ждать разочарование: все воркеры должны быть идентичными. То есть, выбирайте: или все синхронные, или все асинхронные. Хотите тех и других — делайте 2 сервиса и ставьте перед ними nginx. Если всё равно нужен nginx — подумал я, не лучше ли сразу взять nginx unit? Этот аргумент подействовал, и так мы стали использовать nginx unit.

Некоторые читатели, конечно, видят изъян в моих рассуждениях: не обязательно иметь и синхронные, и асинхронные сервисы. Некоторые прекрасно обходятся только последними: заворачивают синхронные операции, если они есть, в асинхронные и горя не знают. Выполняются синхронные операции при этом в отдельном потоке — я говорю сейчас об «адаптерах» вроде sync_to_async. Но лучше я сначала немного расскажу о nginx unit.

Прекрасная документация, удобный API, очень настраиваемый — с nginx unit действительно приятно работать. В интернете пишут, что он хорошо ведёт себя в бенчмарках — я этого не проверял. У nginx unit действительно есть понятие логического «приложения», которое, скорее всего, всегда соответствует отдельному запущенному процессу. Таких приложений может быть запущено множество, и запросы могут роутиться на определённые из них, исходя из каких-то критериев. Я сделал 2 приложения, не оригинально назвав их wsgi и asgi. При этом, они слушают один и тот же порт — чудеса.

Немного о самих протоколах WSGI и ASGI. Нет, особо рассказывать не буду: ну, имеет наше сообщество тягу к использованию протоколов, это же прекрасно. Последний из двух — ASGI — появился вообще случайно, и преследовал другие цели, насколько мне известно. Вебсокеты мы реализовывать не будем — пусть веб-сервис этим занимается — а нам даст вместо соединения корутину, которая умеет читать и писать — примерно такова его идея. Он не является официальным стандартом: для него нет соответствующего PEP. Тем не менее, стандарты вроде WSGI и ASGI существенно облегчают создание веб-серверов вроде nginx unit.

{     "listeners":{         "*:8000":{             "pass":"routes/proj"         }     },     "routes": {         proj: {...}     },     "applications":{         "wsgi":{             "type":"python 3",             "protocol": "wsgi",             "path":"/app",             "module": "proj.wsgi",             "callable": "application"         },         "asgi":{             "type":"python 3",             "protocol": "asgi",             "path":"/app",             "module": "proj.asgi",             "callable": "application"         }     } }

Вот так выглядел примерный конфиг того, что получилось. Как видите, в нём действительно есть wsgi и asgi приложения. Напомню, что был и альтернативый вариант — иметь только асинхронный сервис (asgi), выполняя все функции с синхронным I/O в отдельном потоке. Итак, мы подошли к довольно интересному вопросу: нужно ли нам вообще WSGI приложение? Давайте сравним эти два варианта. Схеме с wsgi приложением дадим кодовое название «первый вариант», а схеме без него — второй. Насколько я знаю, общепринятым является именно второй вариант — где есть только ASGI-приложение (если я неправ, поправьте меня).

Итак, в чём же разница?

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

  2. Давайте подумаем, насколько асинхронный воркер (второй вариант) подходит для обработки «синхронных» запросов? Что представляет из себя такой воркер? Асинхронный поток и несколько синхронных потоков. Насколько такой воркер знает о своей нагрузке (сколько запросов у него в очереди, может ли он принимать новые) ? Ничего не знает. Можно только надеяться, что нагрузка на воркеры будет более-менее однородной. Поскольку у такого воркера есть всего парочка синхронных потоков-обработчиков, то случайно возникшая где-то излишняя нагрузка будет влиять сильно и может привести к таймауту запроса (это если таймаут настроен, а если нет — ещё хуже). В первом же случае (синхронные воркеры) всё очень просто: воркер или занят, или свободен. Конечно, новые запросы распределяем на свободные воркеры. Воркеров много, поэтому они хорошо сглаживают неоднородную нагрузку.

Резюмируя: есть веские причины обрабатывать всю синхронную нагрузку вне асинхронных потоков, а последние по максимуму использовать для асинхронной нагрузки — для чего, собственно, они и предназначены. Справедливости ради, иногда при обработке асинхронных запросов мы всё-таки используем функции с синхронным I/O — конечно, для этого приходится их запускать в отдельном потоке. Но чем такой нагрузки меньше — тем лучше.

Именно это я имел в виду, когда говорил, что прочитав статью, Вы, возможно, захотите деплоить Ваше приложение по-другому (захотите добавить WSGI-приложение). Об этом можно проголосовать в конце статьи. Сейчас же давайте остановимся на некоторых практических аспектах. Мы остановились на том, что у нас есть 2 приложения, WSGI и ASGI, которые слушают один и тот же порт. Но как понять, какой запрос обрабатывать в WSGI-приложении, а какой в ASGI? Какой эндпоинт асинхронный, а какой нет? Об этом знает наше питоновское приложение (потому что у асинхронного эндпоинта функция-обработчик асинхронная). Но nginx unit не знает. Можно ли как-то решить этот вопрос, не прибегая к использованию специальных урлов для асинхронных эндпоинтов?

Оказывается, что с nginx unit — можно, и достаточно несложно. Дело в том, что, как я писал, nginx unit очень конфигурируемый, и роутинг запросов — одна из наиболее конфигурируемых его частей. Конфигурационный файл — это json, его можно сгенерировать автоматически — целиком или какие-то его части. Так мы и сделали — сгенерировали config.json, в котором структура routes отражают структуру urls.py в нашем Django приложении.

Если кому-то интересно, как может выглядеть такой генератор для routes — то примерно вот так https://github.com/pwtail/newunit/blob/master/generate_routes.py Это черновой вариант — более продвинутый, чем тот, что у нас на проде, но менее отлаженный.

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

Не могу не сказать пару слов об особенностях «поддержки» асинхронности в Django. Этот фреймворк очень заботится о разработчиках, поэтому пытается застраховать их от возможных ошибок. Например, если Вы сделали функцию-обработчик асинхронной, но что-то не сложилось для асинхронной обработки Вашего запроса (например, есть неподходящее middleware), Django молчаливо адаптирует вашу асинхронную функцию в синхронную. Если у Вас асинхронный I/O, но Вы задеплоили WSGI-приложение — тоже адаптирует, если синхронный I/O в ASGI-приложении — аналогично. Это может быть удобно для dev-сервера, но для продакшна подход немного странный, на мой взгляд. Стоит ли говорить, что с нашим автоматическим роутингом запросов в нужное приложение, такая «дружественность к разработчику» ничего полезного, кроме того, что прячет ошибки, не делает. Отключить такое поведение непросто (из коробки — никак).

В остальном же — да, можно сказать, что Django поддерживает асинхронность. А nginx unit — действительно очень настраиваемый. Можно даже разные HTTP-методы для одного урла в разные приложения направлять. Но такое сам Django, увы, не поддерживает: нельзя, чтобы, скажем, метод GET обрабатывался синхронной функцией, а метод POST — асинхронной. С последним фактом я, конечно, мириться не стал, и зафайлил на это баг: https://code.djangoproject.com/ticket/33780. Его закрыли через 5 минут (да, я умею настраивать нужные параметры в баг-трекере) как дубликат: оказывается, 16 лет назад уже предлагали что-то похожее.

Напоследок — как и обещал, опрос.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как правильно деплоить веб-приложение с асинхронностью?
66.67% WSGI + ASGI 2
33.33% Только ASGI приложением 1
Проголосовали 3 пользователя. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/post/671604/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *