Стоит ли использовать асинхронную поддержку Django?

Как и я, вы могли рассмотреть возможность использования Django, если вам нужна полнофункциональная среда для быстрой веб-разработки.
К сожалению, Django не славится производительностью — возможно, из-за его синхронного обслуживания по умолчанию (Sync). Это то, через что проходит введение (https://docs.djangoproject.com/en/4.1/intro/tutorial01/).
Синхронизация работает плохо, потому что поток сервера может обслуживать только один запрос за раз. Важно отметить, что запросы ввода-вывода блокируют поток. Пропускную способность можно увеличить, добавив потоки, но потоки сервера Python занимают почти такой же объем оперативной памяти, как и серверный процесс Python. Это делает невозможным увеличение пропускной способности за счет добавления потоков.
Асинхронный дизайн (Async) был добавлен в Django 3.0 (https://docs.djangoproject.com/en/3.0/topics/async/), и во введении не упоминается. Это упоминается как последняя тема в использовании Django (https://docs.djangoproject.com/en/4.1/topics/).
Async может превзойти синхронизацию, поскольку один поток сервера Async может обслуживать несколько запросов одновременно. Во время ожидаемых запросов ввода-вывода await возвращает управление потоку и позволяет выполнять другие запросы. Это похоже на цикл событий Javascript, но не совсем (https://stackoverflow.com/questions/68139555/difference-between-async-await-in-python-vs-javascript).
Так что возникли вопросы:
- Какой прирост производительности может получить Django при использовании асинхронной установки?
- Имеет ли значение, что делает маршрут/конечная точка (нагрузка на ЦП или интенсивный ввод-вывод)?
- А что если задержка низкая (сервер рядом) или высокая задержка (сервер далеко)?
Поэтому я провел эксперимент с нагрузочным тестом. Вы можете найти код и подробности здесьhttps://github.com/nalkhish/Asynchronize-Django. Вкратце, это то, что я исследовал:
For each setup (sync, async limited, async unlimited):
For each request route (high cpu, high io create, high io read):
For each added latency (200, 100, 0):
Emulate load and generate report
- Для Sync Gunicorn использовался для раскрутки 1 Sync worker (https://docs.gunicorn.org/en/stable/design.html).
- Для Async Limited Gunicorn использовался для раскрутки 1 рабочего Uvicorn (https://www.uvicorn.org/deployment/)
- Для Async Unlimited Gunicorn использовался для раскрутки 1 рабочего Uvicorn (https://www.uvicorn.org/deployment/), но postgres был настроен на разрешение 1000 одновременных подключений (больше, чем 100 по умолчанию).
Было использовано 3 маршрута:
- cpu — указывает, что маршрут загружен процессором
def cpu(request: HttpRequest):
"""High cpu route
CPU: High
I/O: low
"""
ct = 0
for i in range(10**7):
ct += i * i
return JsonResponse({"ct": ct})
# In sync
def io_create(request: HttpRequest):
"""Create 1 post
CPU: low
I/O: High
"""
new_post = Post.objects.create(
title=request.POST["title"],
content=request.POST["content"],
)
return JsonResponse(new_post.to_dict())
# In async
async def io_create(request: HttpRequest):
"""Create 1 post
CPU: low
I/O: High
"""
new_post = await Post.objects.acreate(
title=request.POST["title"],
content=request.POST["content"],
)
return JsonResponse(new_post.to_dict())
# In sync
def io_read(request: HttpRequest):
"""Get first 25 posts
CPU: low
I/O: High
"""
posts = Post.objects.all()[:25]
ct = Post.objects.count()
return JsonResponse(
{
"ct": ct,
"posts": [post.to_dict() for post in posts],
}
)
# In Async
async def io_read(request: HttpRequest):
"""Get first 25 posts
CPU: low
I/O: High
"""
posts = []
async for p in Post.objects.all()[:25]:
posts.append(p)
ct = await Post.objects.acount()
return JsonResponse(
{
"ct": ct,
"posts": [post.to_dict() for post in posts],
}
)
docker exec <container_name> tc qdisc add dev eth0 root netem delay 100ms
Где <container_name> — имя контейнера.
Эмуляция загрузки
Нагрузка эмулировалась с помощью инструмента с открытым исходным кодом Locust (https://locust.io/). Саранча бомбардирует порты запросами. Как только «пользователь» заканчивает свою задачу, она начинается снова.
Общая нагрузка длилась 3 минуты:
- Начал с 0 пользователей.
- В течение первых 100 секунд количество пользователей увеличивается на 5 пользователей в секунду до 500 одновременных пользователей.
- В течение последних 80 секунд количество пользователей поддерживалось на уровне 500.
Результаты:
Определения:
- Max sRPS: максимальное количество успешных запросов в секунду было получено путем получения максимальной разницы между общим количеством запросов и неудачными запросами для всех графиков отчета (см. дополнительные цифры в репозитории ).
- Общее количество успешных запросов: общее количество успешных запросов было получено путем вычитания #failed request из #total Requests (см. дополнительные цифры в репозитории ).
Таблица 1: Привязка к процессору. Макс. sRPS и общий успех для комбинаций настройки (синхронная/асинхронная ограниченная/асинхронная неограниченная) и задержки (0 мс, 100 мс, 200 мс). Async на самом деле работает плохо.

Асинхронный ограниченный против асинхронного неограниченного:
- Почему Async без ограничений работает лучше, чем Async с ограничениями? Асинхронный неограниченный доступ позволяет больше подключений к базе данных, но маршрут ЦП и установка промежуточного программного обеспечения не используют базу данных. Это необходимо исследовать дополнительно.
- В любом случае обе установки Async имеют непредсказуемую динамику (см. дополнительные рисунки на репо).https://github.com/nalkhish/Asynchronize-Django).
- Синхронизация имеет более низкий максимальный показатель sRPS, чем безлимитная асинхронность. Вероятно, это связано с тем, что асинхронные серверы могут обрабатывать несколько запросов одновременно, поэтому несколько запросов завершались одновременно. Это удивительно, потому что asyncio предположительно не переключает контекст, пока не встретится с оператором await, которого нет в маршруте процессора. Это необходимо исследовать дополнительно.
- Синхронизация имела предсказуемую динамику и в целом была более успешной, чем асинхронная. Этого достаточно, чтобы гарантировать использование синхронизации для сервисов, привязанных к процессору.
Таблица 2. Чтение БД с привязкой к вводу-выводу. Макс. sRPS и общий успех для комбинаций сервера (асинхронный/синхронный) и задержки (0 мс, 100 мс, 200 мс). При использовании Async вместо Sync пропускная способность выше при больших задержках.

Таблица 3: Создание БД с привязкой к вводу-выводу. Макс. sRPS и общий успех для комбинаций сервера (асинхронный/синхронный) и задержки (0 мс, 100 мс, 200 мс). При использовании Async пропускная способность выше при больших задержках.

Асинхронный ограниченный по сравнению с асинхронным неограниченным: Асинхронный безлимитный имел более высокий максимальный показатель sRPS и общий успех, чем асинхронный с ограниченным доступом.
- Для маршрута чтения, связанного с вводом-выводом, это, вероятно, может быть связано с тем, что база данных является узким местом, поскольку она дает сбой.
- Для маршрута создания с привязкой к вводу-выводу это необходимо дополнительно изучить, поскольку база данных не давала сбоев для Async limited (см. дополнительный рисунок).
- Как для маршрутов io_read, так и для маршрутов io_create Sync имел гораздо более низкую производительность, чем Async (при добавленной задержке в 200 мс разница в общей пропускной способности была 40-кратной для io_read и 230-кратной для io_create).
- Вероятно, это связано с тем, что рабочий поток сервера ожидал завершения запросов к базе данных, прежде чем он мог обработать следующий запрос. Эта теория подтверждается обратной зависимостью между задержкой и максимальным числом запросов в секунду и общим успехом синхронизации.
Ограничения более подробно описаны в файле readme репозитория.https://github.com/nalkhish/Asynchronize-Django(небольшое количество информации, обсуждаемой здесь, отсутствие логики повторных попыток и отсутствие входящей задержки), но это исследование в достаточной степени исследует возможности асинхронного и синхронного обслуживания Django.
Если вы используете Django и имеете нагрузку на ЦП, используйте Sync. В противном случае, если нагрузки связаны с вводом-выводом, используйте Async. Скорее всего, это более чем в 10 раз увеличит вашу пропускную способность.