Как должны выглядеть современные сервисы на питоне, многие имеют представление. Все они так или иначе имеют поддержку асинхронных операций. А вот как их лучше деплоить? Здесь некоторые руководства (как 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-приложение (если я неправ, поправьте меня).
Итак, в чём же разница?
-
В первом варианте синхронные воркеры значительно снимают нагрузку с асинхронных. Это может быть очень полезно, потому что если в памяти нашего асинхронного приложения хранится что-то полезное, то это не получится масштабировать за пределы одного потока. Поэтому разгрузить асинхронный поток бывает очень кстати.
-
Давайте подумаем, насколько асинхронный воркер (второй вариант) подходит для обработки «синхронных» запросов? Что представляет из себя такой воркер? Асинхронный поток и несколько синхронных потоков. Насколько такой воркер знает о своей нагрузке (сколько запросов у него в очереди, может ли он принимать новые) ? Ничего не знает. Можно только надеяться, что нагрузка на воркеры будет более-менее однородной. Поскольку у такого воркера есть всего парочка синхронных потоков-обработчиков, то случайно возникшая где-то излишняя нагрузка будет влиять сильно и может привести к таймауту запроса (это если таймаут настроен, а если нет — ещё хуже). В первом же случае (синхронные воркеры) всё очень просто: воркер или занят, или свободен. Конечно, новые запросы распределяем на свободные воркеры. Воркеров много, поэтому они хорошо сглаживают неоднородную нагрузку.
Резюмируя: есть веские причины обрабатывать всю синхронную нагрузку вне асинхронных потоков, а последние по максимуму использовать для асинхронной нагрузки — для чего, собственно, они и предназначены. Справедливости ради, иногда при обработке асинхронных запросов мы всё-таки используем функции с синхронным 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 лет назад уже предлагали что-то похожее.
Напоследок — как и обещал, опрос.
ссылка на оригинал статьи https://habr.com/ru/post/671604/
Добавить комментарий