Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров7.4K

Как должны выглядеть современные сервисы на питоне, многие имеют представление. Все они, так или иначе, имеют поддержку асинхронных операций. А вот, как их лучше деплоить? Здесь некоторые руководства (как 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 лет назад уже предлагали что-то похожее.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как правильно деплоить веб-приложение с асинхронностью?
42.86% WSGI + ASGI21
57.14% Только ASGI приложением28
Проголосовали 49 пользователей. Воздержались 25 пользователей.
Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии5

Публикации

Истории

Работа

Python разработчик
120 вакансий
Data Scientist
78 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань