Сообщить об ошибке.

Использование async и await в приложении на Flask.

Маршруты, обработчики ошибок, функции до и после запроса, а также функции освобождения ресурсов могут быть функциями сопрограмм, если Flask установлен с дополнительным параметром async (pip install flask [async]). Для этого требуется Python 3.7+, где доступен contextvars.ContextVar. Это позволяет определять представления с помощью async def и использовать await.

Синтаксис:

@app.route("/get-data")
async def get_data():
    data = await async_db_query(...)
    return jsonify(data)

Подключаемые представления на основе классов также поддерживают обработчики, реализованные как сопрограммы. Это относится к методу dispatch_request() в представлениях, которые наследуются от класса flask.views.View, а также ко всем обработчикам методов HTTP в представлениях, которые наследуются от класса flask.views.MethodView.

Использование async в Windows на Python 3.8

В Windows в Python 3.8 есть ошибка, связанная с модулем asyncio. Если столкнетесь с чем-то вроде ValueError: set_wakeup_fd, то обновите Python до версии 3.9.

Содержание:

Производительность.

Для асинхронных функций требуется цикл событий. Фреймворк Flask, как приложение WSGI, использует одну задачу/воркер для обработки одного цикла запроса/ответа. Когда запрос поступает в асинхронное представление, то Flask запускает цикл обработки событий в отдельном потоке, запускает там функцию представление, а затем возвращает результат.

Каждый запрос по-прежнему связывает одну задачу, даже для асинхронных представлений. Положительным моментом является то, что асинхронный код можно запускать в самом представлении, например, для выполнения нескольких одновременных запросов к базе данных и/или HTTP-запросов к внешнему API и т. д. НО количество запросов, которые веб-приложение может обрабатывать одновременно, останется прежним.

Использование async def по своей сути не быстрее, чем синхронный код. Асинхронный режим полезен при выполнении параллельных задач, связанных с вводом-выводом, но, вероятно, не улучшит задачи, связанные с процессором. Традиционные представления Flask по-прежнему подходят для большинства случаев использования, но поддержка async в Flask позволяет писать и использовать код, что раньше было невозможно изначально.

Фоновые задачи.

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

Если необходимо использовать фоновые задачи, то лучше всего использовать очередь задач для запуска фоновой работы, а не создавать задачи в функции-представлении. Принимая сказанное во внимание, можно запускать задачи asyncio, обслуживая Flask с сервером ASGI и используя адаптер WsgiToAsgi, как описано в ASGI. Так как адаптер создаст непрерывный цикл обработки событий, то это точно будет работать.

Например:

Адаптер WsgiToAsgi интегрируется с циклом событий, используемым для Flask. Что бы использовать адаптер, необходимо обернуть приложение Flask:

from asgiref.wsgi import WsgiToAsgi
from flask import Flask

app = Flask(__name__)

...

asgi_app = WsgiToAsgi(app)

а затем запускать приложение asgi_app с сервером asgi, например, с помощью Hypercorn.

$ hypercorn module:asgi_app

Расширения Flask и асинхронность.

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

Авторы расширений могут поддерживать асинхронные функции, используя метод flask.Flask.ensure_sync(). Например, если расширение предоставляет декоратор для функции-представления, то необходимо добавить app.ensure_sync() перед вызовом функции-декоратора.

Асинхронный фреймворк Quart.

Поддержка async в Flask менее эффективна, чем фреймворки, которые изначально разрабатывались как async-first из-за способа реализации асинхронности. Если у вас в основном асинхронная кодовая база, то имеет смысл рассмотреть использования модуля Quart. Фреймворк Quart - это точная реализация Flask, основанная на стандарте ASGI вместо WSGI. Такая реализация позволяет ему обрабатывать множество одновременных запросов, длительных запросов и веб-сокетов, не требуя нескольких процессов или потоков.

Также можно запустить приложение Flask, написанное по асинхронной схеме с gevent или eventlet, для достижения преимущества обработки асинхронных запросов. Эти библиотеки исправляют низкоуровневые функции Python для достижения цели асинхронной обработки. Решение о том, следует ли использовать Flask, Quart или что-то еще, зависит от понимания конкретных потребностей разрабатываемого веб-проекта.

Асинхронный запуск синхронного приложения Flask.

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

Использование асинхронного рабочего процесса по любому улучшит производительность, так как веб-приложения часто выполняют много параллельных операций ввода-вывода (ожидание подключения по сети, ожидание подключения и выполнения запроса к БД, ожидание чтения templantes с диска и т.д.). Для этих целей можно использовать асинхронные модули gevent или eventlet совместно с gunicorn.

Нашей команде нравится использовать асинхронные процессы gevent в сочетании с несколькими процессами gunicorn. Другими словами, gunicorn обеспечивает многопроцессорность, а gevent асинхронность на каждый процесс gunicorn.

Почему gevent, а не eventlet?

Модуль gevent построен поверх системной библиотеки libevent, а с версии 1.0 использует libev и c-ares. Системная библиотека libev (так-же используется в Nginx) - популярный портативный цикл обработки событий. Он запускает приложение, используя самый быстрый механизм, доступный в системе, такой как epoll в Linux и kqueue во FreeBSD. В отличие от gevent, модуль Eventlet поддерживает свои собственные циклы событий на чистом Python и только недавно получил поддержку epoll. Все циклы событий gevent были хорошо протестированы в реальных масштабных средах.

Превосходная производительность - одно из преимуществ тесной интеграции с libev, но не единственное. Другие преимущества:

  • Обработка сигналов интегрирована с циклом обработки событий.
  • Другие библиотеки на основе libev могут интегрироваться с приложением через единый цикл обработки событий.
  • Запросы DNS обрабатываются асинхронно, а не через пул блокирующих вызовов.
  • Интерфейс gevent соответствует соглашениям, установленным стандартной библиотекой Python.

Установим в виртуальное окружение, в котором стоит Flask пакет gunicorn с поддержкой gevent:

# активируем виртуальное окружение 
$ source .venv/bin/activate
# ставим `gunicorn` с поддержкой `gevent`
$ python3 -m pip install gunicorn[gevent]

Затем, в качестве грубого эксперимента, можно запустить приложение (например, server.py) с несколькими рабочими процессами gunicorn и/или асинхронными рабочими процессами gevent:

# server.py
from flask import Flask
from time import sleep

app = Flask(__name__)

@app.route("/")
def hello():
    # замедляем ответ сервера, это грубая
    # эмуляция ожидания различных соединений.
    sleep(0.5)
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Запуск Flask c Gunicorn: только многопроцессорность.

Запускаем gunicorn с двумя рабочими синхронными процессами (многопроцессорность). Будем считать эту схему эталонной.

$ gunicorn server:app -w 2
[39391] [INFO] Starting gunicorn 20.1.0
[39391] [INFO] Listening at: http://127.0.0.1:8000 (39391)
# Используется синхронный вариант ответа 
[39391] [INFO] Using worker: sync
# и 2 процесса приложения
[39392] [INFO] Booting worker with pid: 39392
[39394] [INFO] Booting worker with pid: 39394

Тестируем при помощи утилиты ab из пакета apache2-utils. Выставляем 50 запросов и 10 одновременных подключений.

$ ab -r -n 50 -c 10 http://127.0.0.1:8000/
# Server Software:        gunicorn
# Server Hostname:        127.0.0.1
# Server Port:            8000
# ...
# Time taken for tests:   13.070 seconds
# ...
# Requests per second:    3.83 [#/sec] (mean)
# Time per request:       2614.073 [ms] (mean)
# Time per request:       261.407 [ms] (mean, across all concurrent requests)
# ...

При работе приложения по схеме "Gunicorn с двумя рабочими процессами" получаем 3.83 запроса в секунду, длительность теста составила 13.070 секунды. Фиксируем расход памяти и нагрузку на процессор.

Запуск Flask c Gunicorn: процессы + gevent.

Запускаем gunicorn с двумя рабочими процессами, плюс 50 асинхронных gevent процессов на синхронный процесс gunicorn (50 * 2 = 100).

$ gunicorn server:app -w 2 -k gevent --worker-connections 100
[40739] [INFO] Starting gunicorn 20.1.0
[40739] [INFO] Listening at: http://127.0.0.1:8000 (40739)
# Теперь используется `gevent` 
[40739] [INFO] Using worker: gevent
[40740] [INFO] Booting worker with pid: 40740
[40741] [INFO] Booting worker with pid: 40741

Тестируем при помощи утилиты ab из пакета apache2-utils. Выставляем 50 запросов и 10 одновременных подключений.

$ ab -r -n 50 -c 10 http://127.0.0.1:8000/
# Server Software:        gunicorn
# Server Hostname:        127.0.0.1
# Server Port:            8000
# ...
# Time taken for tests:   3.034 seconds
# ...
# Requests per second:    16.48 [#/sec] (mean)
# Time per request:       606.827 [ms] (mean)
# Time per request:       60.683 [ms] (mean, across all concurrent requests)
# ...

При работе приложения по схеме "2 процесса gunicorn + 100 асинхронных gevent" получаем 16.48 запроса в секунду, длительность теста составила 3.034 секунды. При этом потребление оперативной памяти снизилось на 1/3 (меньше экземпляров приложений стоят в очереди на обработку), но возросла нагрузка на процессор из за того что процессы приложения стали обрабатываться без ожидающих операций.

Вывод: Схема "Gunicorn + gevent" увеличила производительность приложения в 5 раз + снижение потребления ОЗУ.

Дополнительно можно посмотреть материал "Разворачиваем Flask + Nginx + Gunicorn + Gevent на VDS".

Запуск Flask c Gunicorn: процессы + потоки.

И для чистоты эксперимента запустим gunicorn с двумя рабочими процессами, плюс 50 потоков на синхронный процесс gunicorn (50 * 2 = 100):

$ gunicorn server:app -w 2 --threads 100
[40739] [INFO] Starting gunicorn 20.1.0
[40739] [INFO] Listening at: http://127.0.0.1:8000 (40739)
# Теперь используется `gthread`
[40739] [INFO] Using worker: gthread
[40740] [INFO] Booting worker with pid: 40740
[40741] [INFO] Booting worker with pid: 40741

Тестируем при помощи утилиты ab из пакета apache2-utils. Условия тестирования не меняем.

$ ab -r -n 50 -c 10 http://127.0.0.1:8000/
# Server Software:        gunicorn
# Server Hostname:        127.0.0.1
# Server Port:            8000
# ...
# Time taken for tests:   3.079 seconds
# ...
# Requests per second:    16.24 [#/sec] (mean)
# Time per request:       615.857 [ms] (mean)
# Time per request:       61.586 [ms] (mean, across all concurrent requests)
# ...

При работе приложения по схеме "2 процесса gunicorn + 100 потоков" получаем 16.24 запроса в секунду, длительность теста составила 3.079 секунды (скорость обработки практически не изменилась). При этом потребление оперативной памяти незначительно выросло, но очень сильно возросла нагрузка на процессор (почти в 3 раза по сравнении с первой схемой запуска) из за переключений, вызванных GIL.

Многопоточную схему запуска практиковать не рекомендуют из за большого потребления ресурсов сервера, связанного с GIL.

Вот и все...