Маршруты, обработчики ошибок, функции до и после запроса, а также функции освобождения ресурсов могут быть функциями сопрограмм, если 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.
Quart
;Для асинхронных функций требуется цикл событий. Фреймворк 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, написанные до поддержки 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 можно развернуть с помощью асинхронных рабочих процессов. В частности, выполнение в цикле событий, который дает результат при вводе-выводе, подобно тому, как asyncio
предоставляет стандартную библиотеку.
Использование асинхронного рабочего процесса по любому улучшит производительность, так как веб-приложения часто выполняют много параллельных операций ввода-вывода (ожидание подключения по сети, ожидание подключения и выполнения запроса к БД, ожидание чтения templantes
с диска и т.д.). Для этих целей можно использовать асинхронные модули gevent
или eventlet
совместно с gunicorn
.
Нашей команде нравится использовать асинхронные процессы gevent
или eventlet
в сочетании с несколькими процессами gunicorn
. Другими словами, gunicorn
обеспечивает многопроцессорность, а gevent
/eventlet
асинхронность на каждый процесс gunicorn
.
Установим в виртуальное окружение, в котором стоит Flask пакет gunicorn
с поддержкой gevent
:
# активируем виртуальное окружение $ source .venv/bin/activate # ставим `gunicorn` с поддержкой `gevent` $ python3 -m pip install gunicorn[gevent] # или с поддержкой `eventlet` $ python3 -m pip install gunicorn[eventlet]
Затем, в качестве ГРУБОГО ЭКСПЕРИМЕНТА, можно запустить приложение (например, 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 через
gunicorn
+gevent
/eventlet
, количество асинхронных потоков на процесс подбираются индивидуально под конкретное приложение в зависимости от его "ТЯЖЕСТИ" и характеристик сервера. НЕ НАДО масштабировать количество воркеров до ожидаемого количества клиентов. Модулюgunicorn
требуется всего 4–12 рабочих процессов для обработки сотен тысяч запросов в секунду. В противном случае, чрезмерное количество асинхронных воркеров могут легко положить выделенный сервер из за чрезмерной нагрузки на процессор при средней посещаемости ресурса (20 - 30 тыс. уникальных посетителя, включая роботов). И вообще, документация модуляgunicorn
не советует запускать его черезgevent
илиeventlet
без какой либо необходимости (если приложение отвечает на запросы без задержек)
Выдержка из документации gunicorn
:
Класс воркера
sync
, используемый по умолчанию, будет обрабатывать большинство обычных типов рабочих нагрузок. Доступные асинхронные рабочие процессы вgunicorn
основаны наGreenlets
(через модулиeventlet
иgevent
).Greenlets
- это реализация совместной многопоточности для Python. В общем, приложение должно иметь возможность использовать эти рабочие классы без каких-либо изменений. Например, для полной поддержкиGreenlets
может потребоваться адаптация приложений.При использовании модулей, например,
gevent
иpsycopg
имеет смысл убедиться, что модульpsycogreen
установлен и настроен. Другие приложения могут быть вообще несовместимы, поскольку они, например, полагаются на исходное непропатченное поведение.
То есть, если приложение запускается на асинхронной основе, то и все связанные с ним службы (например, база данных) должны поддерживать асинхронность.
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 секунды. Фиксируем расход памяти и нагрузку на процессор.
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".
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.
Вот и все...