Здравствуй, дорогой читатель, тема этой публикации - DEP-9 и его защита. DEP-9 - это "RFC" для асинхронности в django. Если что, этот RFC не был сделан полностью, поэтому я буду защищать только ту часть, которая сделана: мало ли, как могли бы сделать всё остальное!

Отвечу сразу на вопрос, который я задал в предисловии, потому что поддерживать интригу - не в моём стиле. Вопрос в следующем. Есть распространённое мнение, что, поскольку django так и не смог избавиться от блокирующего ввода-вывода в большей части своей кодовой базы - ORM, то при использовании django в асинхронном приложении ничего не остаётся, как вызывать функции этого ORM в отдельном потоке. Такая чехарда между синхронными и асинхронными потоками не может не сказаться пагубно на производительности, и вообще, не идёт ни в какое сравнение с нативной асинхронностью.

И дам сразу ответ на этот вопрос: это неверное мнение. Django действительно использует блокирующий ввод-вывод при работе с базой данных - некоторые другие фреймворки используют асинхронный. Это равноценные варианты, производительность одинакова плюс-минус - в том числе, при работе с key-value базами, очередями и так далее. Однако, с этим есть проблема, которая, с развитием микросервисов, стала более распространённой. Угадаете, какая?

Это - вызов стороннего сервиса, по http, например. Он длится - не так, чтобы очень долго - пользователь может и подождать. Но это - неоправданно долго в том смысле, что один из потоков простаивает зря. Ещё и время отклика у этого стороннего сервиса не гарантированное. Вот для решения этой проблемы и существуют эти адаптеры и запуск в другом потоке. Что касается производительности - всё как было, так и осталось - одинаковая плюс-минус. Каких-то особенных проблем с этим подходом нет. Это - если кратко, но есть нюансы, читайте дальше.

Всё самое интересное в нюансах. Мы говорили про адаптеры. Под адаптерами я подразумеваю штуки вроде sync_to_async (название не моё, по-моему, авторы этих штук и ввели в употребление). Так вот, они дают возможность иметь в асинхронных вьюшках "вкрапления" блокирующего кода - те самые, которые выполняются в другом потоке. Однако, для целей этой статьи, я попрошу читателя представить себе другой вариант: что у нас - блокирующие функции, в которых - "вкрапления" асинхронного кода. Наоборот, то есть.

С позволения читателя, я буду использовать для иллюстрации свой собственный новый "синтаксис". Потому что не может быть статья на хабре без экзотики. Короче говоря, всё, что находится под асинхронным контекстным менеджером io, выполняется в другом потоке:

async def myview(request):
    # blocking code
    ...
    
    async with io:
        # this goes to separate thread
        async with httpx.AsyncClient() as client:
            response = await client.get(url)
        ...
    
    # blocking code
    ...

"io" - потому что "with asyncio". То, что перед функцией стоит async def - не обращайте на это внимания: функция, на самом деле, блокирующая. Да, такой дурацкий синтаксис. Если интересно, про него можно прочитать в моей новогодней статье. В рамках же этой статьи, читатель имеет полное моральное право с ним не соглашаться: синтаксис не важен, мы могли с тем же успехом использовать для асинхронного кода отдельную функцию.

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

Как они могут выполняться? Скорее всего, у нас будет тредпул из потоков-воркеров, которые будут выполнять блокирующий код - пусть в нём будет 2 или 3 потока. Также нам нужен поток, который будет выполнять асинхронный код - с event loop-ом и корутинами. Отвлечёмся пока от существующих стандартов: не будем ограничивать себя WSGI, ASGI или чем-то другим. Каким может быть порядок выполнения:

  1. 1-я блокирующая секция выполняется и запускает асинхронную задачу, представленную 2-й секцией.

  2. Поток-воркер, который выполнял 1-ю секцию, теперь свободен, и берёт в обработку блокирующие секции других вьюшек

  3. Тем временем, асинхронная 2-я секция выполнилась и ставит в очередь на выполнение в тредпул 3-ю секцию.

Итак, 3-я секция стоит в очереди в тредпуле, ожидая свободного воркера - придётся немного подождать. В рамках блокирующего подхода, нас это не сильно смущает: нам важно, чтобы воркеры были максимально загружены, и чтобы их нагрузка была разбита на достаточно малые части. В таком случае, мы можем расчитывать в итоге на нормальную производительность.

Учитывая, что причина, по которой мы вообще используем несколько секций внутри функции - это наличие долгих операций (http запрос, в нашем примере), временем на "переключение" между потоками можно пренебречь. Но обратите внимание: количество блокирующих секций внутри функции важно! Именно перед выполнением блокирующей секции мы ждём, пока не освободится воркер. Так что, если есть возможность сэкономить на какой-нибудь блокирующей секции, то делайте это - это уже совет, который применим к "реальной жизни" - то есть, Вашему проекту на django.

Вернёмся теперь снова к реальности и вспомним, что обычно мы имеем дело с ASGI приложениями и асинхронными вьюшками. Что это меняет? Ну, как минимум, теперь (асинхронные) вьюшки начинаются и заканчиваются асинхронной секцией - увеличивается количество асинхронных секций. Количество блокирующих секций остаётся тем же.

HOUSTON WE HAVE A PROBLEM
Автор сам немного запутался. Возможно, выйдет другая статья, где главный мессадж поменяется на противоположный
EPIC FAIL

____________ UPDATE ____________

Это не издевательство: я действительно изменил своё мнение на противоположное о вышеуказанной реализации (DEP9), когда статья уже была опубликована, и не придумал ничего лучше, как поставить такую заглушку.

Насчёт того, что автор запутался - это преувеличение. Всё вышенаписанное остаётся справедливым. Закончить повествование несложно: в общем, у нас асинхронные вьюшки - это значит, что выполнение начинается и заканчивается в асинхронном потоке. Возможно, есть накладки по производительности, но они небольшие (проблема не в них). С другой стороны, если мы знаем, какой секцией начинаются и заканчиваются вьюшки, у нас гораздо больше определённости: есть возможность писать мидлвэр для них, понятно как деплоить и т. д.

В общем, это так, но не совсем. Всё напутали разработчики django. Чем они только занимаются на своих конференциях? "Хоть бери и сам стихи с музыкой сочиняй" (Б. Н. Ельцин)

Подробности будут в следующей статье.

_________________________________

Комментарии (5)


  1. jilev
    20.01.2023 06:25

    Весьма интересно


  1. versetty777
    20.01.2023 08:15

    О чём спорят мои коллеги по цеху по асинхронному Django:

    1. Эффективность: сотрудники могут спорить о том, насколько эффективно использование асинхронной модели в Django увеличивает производительность приложения и уменьшает нагрузку на сервер.

    2. Сложность: сотрудники могут спорить о том, насколько сложно реализовать асинхронность в Django и насколько это затратно по времени и ресурсам.

    3. Обратная совместимость: сотрудники могут спорить о том, насколько асинхронный Django совместим со старыми версиями Django или с другими библиотеками и фреймворками.

    4. Надежность: сотрудники могут спорить о том, насколько надежным является асинхронный Django и насколько он может быть устойчив к ошибкам.


    1. AcckiyGerman
      20.01.2023 12:06

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

      • какой шаблонизатор подключить

      • какой ORM

      • встроенные тесты vs pytest

      • sync vs async

      • gunicorn vs unicorn vs hypercorn или может старый добрый apache_wsgi

      Одни решения влияют на другие и добавление асинхронности добавило в эту кучу решений кучу неоднозначности ибо "батарейки" писались еще когда про Async в питоне не слыхивали. По хорошему в этом месте желающим в Async контрибьюторам нужно было разделиться с основной командой и написать Async версию с нуля, а не пытатся изначально синхронный фреймворк переделывать сверху вниз по кусочкам.

      Например DRF (де-факто стандарт для REST API на django, хотя, как вы догадываетесь, можно прикрутить и что-нибудь другое; про который наверное уже отдельные курсы в интернете продают, ибо это фреймворк во фреймворке). Ребята из DRF всё обсуждают, как асинхронность прикрутить, а разработчикам предлагается лепить sync_to_async адаптер на каждую VIEW.

      Ну а пока разработчики "батареек" обновляют свои продукты (но не все) до актуальной версии Django, Django Software Foundation выпускает новую.

      Разработчики Django забыли про "дзен питона" во многих пунктах, наступили на те же грабли, что наступил сам Гвидо, обновляя python2 до 3 и идут по стопам Angular, который на сайте прячет свою версию (сейчас 14-я), чтобы людей не отпугивать.

      К сожалению такова судьба многих популярных проектов, ибо очень сложно удержаться от искушения прикрутить еще одну мааааленькую свистелку и пару перделок. Ну и внутреннее API заодно переписать под последний модный подход.


      1. AcckiyGerman
        20.01.2023 12:11

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


        1. whoisking
          20.01.2023 12:44
          +1

          Занимался и тем и тем, одинаково "приятно")