Введение

В будущем разработки Django Framework произойдут некоторые интересные изменения. Одним из заметных изменений является обновленная поддержка операций асинхронного доступа к данным и обработчиков методов HTTP. В этой статье мы рассмотрим некоторые созданные мной тесты и сравним время выполнения как асинхронных, так и синхронных процессов.


Разница между синхронным и асинхронным в Django

Синхронное программирование следует строгому набору последовательностей (или, другими словами, оно выполняется последовательно), где операции выполняются по одной в строгом порядке. Следующая цитата Дэвида Беванса с сайта «mendix.com» дает отличное описание синхронного программирования:

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

Асинхронное программирование позволяет выполнять несколько связанных операций одновременно, не дожидаясь завершения предыдущих задач. Мы описываем его как многопоточную модель, реализующую неблокирующую архитектуру. Если поток свободен и может выполнить операцию — он ее выполняет, при этом приостановив операцию, которую ожидает. Еще одно отличное описание асинхронного программирования от Дэвида Беванса:

Текстовые сообщения — это асинхронный метод общения. Один человек может отправить текстовое сообщение, а получатель может ответить на свое усмотрение. Тем временем отправитель может заниматься другими делами, ожидая ответа.

При этом мы можем заметить, что асинхронность должна значительно повысить производительность при правильном использовании.

Django по своей сути работает синхронно, и где-то в версии 3.0 (когда такие расширения, как «twisted», «channels» и «asyncio» начали набирать обороты), начала проявляться идея, что Django может работать асинхронно.

Из моего исследования лучший способ решить, где использовать синхронизацию или асинхронность, — это определить, является ли представление (и, соответственно, чего мы хотим достичь с помощью этого представления) Привязка ввода-вывода или привязка к процессору. При решении проблем, связанных с вводом-выводом, следует использовать асинхронность.


Среда и предпосылки

Прежде чем мы начнем тестирование, давайте рассмотрим настройку и некоторые важные примечания. Перво-наперво, очевидное замечание (но важное): результаты могут различаться от IDE к IDE, а также от настройки связанных/используемых пакетов в своем проекте, фоновых процессов и стороннего программного обеспечения. Следовательно, время выполнения, которое вы видите здесь, может быть или не быть таким же, как и время, которого вы достигнете, если решите воссоздать эти тесты.

Для этих тестов мы используем Python 3.9 как наш переводчик и PostgreSQL 13.6 как наша система баз данных. Установленные пакеты Python выглядят следующим образом:

Django~=4.1
psycopg2~=2.9.3
hypercorn~=0.14.1

psycopg2 действует как наш адаптер базы данных, и hypercorn будет эмулировать тестовый сервер ASGI для нашей среды.

Вы можете удивиться и сказать:

Да, но зачем использовать гиперкорн, если в нашем проекте уже есть asgi.py?

Это правда, asgi.py в нашем проекте действительно содержит вызываемое приложение и может использоваться любым сервером ASGI в разработке или производстве. Но он не используется сервером разработки (который мы вызываем с помощью runserver команда), и именно здесь появляется гиперкорн. myproject.asgi:applicationсервер запущен и готов к локальному использованию.

Важная заметка: правильный способ измерения производительности и времени выполнения — через TestCase (или даже более многофункциональный TransactionTestCase) и тестирования модулей/модулей в целом. Но у меня есть некоторые опасения по поводу AsyncRequestFactory используется в тестах и AsyncClient(). Читая документацию, кажется, что async поведение частично эмулируется, когда выполняются асинхронные функции, а затем процесс возвращается к синхронному поведению. Сказать, что самый аутентичный/реалистичный способ проверить это — сделать простое представление и вызвать его через URL с помощью hypercorn эмулированный сервер ASGI. использованная литература [3] а также [4] содержат больше информации о «Тестировании асинхронного кода» и «Расширенных темах тестирования» — возможно, я пропустил какой-то важный контекст.

Модели

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

from django.db.models import Model, TextField, CharField, DateField, ManyToManyField, ForeignKey, CASCADE
from django.utils.timezone import now


# Create your models here.
class CarParts(Model):
    engine = TextField()


class Manufacturer(Model):
    name = CharField(max_length=150)


class Store(Model):
    location = CharField(max_length=150)


class Car(Model):
    created_at = DateField(default=now)
    store = ManyToManyField(Store, null=True)
    car_parts = ForeignKey(CarParts, on_delete=CASCADE, null=True)
    manufacturer = ForeignKey(Manufacturer, on_delete=CASCADE, null=True)

Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы будем запрашивать и повторять 50, 500 и 1500 объектов на модель, чтобы мы могли отслеживать, как она масштабируется с увеличением количества данных.

Тестирование

У нас есть два представления класса, расширяющие базовый класс View из django.views, где каждый будет содержать несколько методов. Django не позволяет смешивать синхронные и асинхронные методы — помните, что синхронизация реализует блокирующий подход, а неблокирующий — в асинхронном. Таким образом, одно представление класса будет иметь все асинхронные методы и другие методы синхронизации.

Испытания будут проводиться в следующем порядке:

  1. Загрузить x количество объектов
  2. Выполните три последовательных теста (несколько тестов, чтобы компенсировать потенциальные фоновые процессы IDE, которые могут привести к снижению производительности).
  3. Анализировать

Тест A: цикл CRUD с использованием ORM с помощью методов get и post

Глобальные функции, которые будут вызываться в представлениях (одна асинхронная, другая синхронная):

async def async_iteration():
    car_obj = Car.objects.all()
    async for i in car_obj:
        a_query = await i.store.afirst()
    return car_obj

def sync_iteration():
    car_obj = Car.objects.all()
    for i in car_obj:
        a_query = i.store.first()
    return car_obj
Войти в полноэкранный режим

Выйти из полноэкранного режима

Наш класс синхронизации:

class PerformanceTestSync(View):
    def get(self, request):
        e_list = []
        start = time.time()
        for entry in Car.objects.all():
            try:
                a_query = entry.store.first()
                b_query = CarParts.objects.filter(
                    Q(engine__contains='engine') & 
                    Q(car__manufacturer_id__gt=20)).first()
            except (Exception, KeyError) as e:
                print('Error during sync iteration: ', e)
        co = sync_iteration()
        for v in co:
            e_list.append(v)
        end = time.time()
        print('Sync GET time: ', end - start)

    def post(self, request):
        start = time.time()
        engine = request.POST.get('engine')
        name = request.POST.get('name')
        location = request.POST.get('location')
        try:
            car_parts_obj = CarParts.objects.create(engine=engine)
        except (CarParts.DoesNotExist, ObjectDoesNotExist, 
                Exception) as e:
            print('CarParts failed to create: ', e)
            car_parts_obj = None
            pass
        try:
            manufacturer_obj = Manufacturer.objects.create(
                                                 name=name)
        except (Manufacturer.DoesNotExist, ObjectDoesNotExist,
                             Exception) as e:
            print('Manufacturer failed to create: ', e)
            manufacturer_obj = None
            pass
        try:
            store_obj = Store.objects.create(location=location)
        except (Store.DoesNotExist, ObjectDoesNotExist, 
                Exception) as e:
            print('Store failed to create: ', e)
            store_obj = None
            pass

        if car_parts_obj and manufacturer_obj:
            try:
                car_obj = Car.objects.create(
                            car_parts=car_parts_obj, 
                            manufacturer=manufacturer_obj
                          )
                car_obj.store.add(store_obj)

            except (Car.DoesNotExist, ObjectDoesNotExist, 
                    Exception) as e:
                print('Car failed to create: ', e)
                pass

        end = time.time()
        print('Sync POST time: ', end - start)
        return HttpResponse("ok", status=200)
Войти в полноэкранный режим

Выйти из полноэкранного режима

Наш асинхронный класс:

class PerformanceTestAsync(View):
    async def get(self, request):
        e_list = []
        start = time.time()
        async for entry in Car.objects.all():
            try:
                a_query = await entry.store.afirst()
                b_query = await CarParts.objects.filter(
                        Q(engine__contains='engine') & 
                        Q(car__manufacturer_id__gt=20)).afirst()
            except (Exception, KeyError) as e:
                print('Error during async iteration: ', e)
        co = await async_iteration()
        async for v in co:
            e_list.append(v)
        end = time.time()
        print('Async GET time: ', end - start)

        return render(request, 'demo_template.html')

    async def post(self, request):
        start = time.time()
        engine = request.POST.get('engine')
        name = request.POST.get('name')
        location = request.POST.get('location')
        try:
            car_parts_obj = await CarParts.objects.acreate(
                                 engine=engine
                                )
        except (CarParts.DoesNotExist, ObjectDoesNotExist, 
                Exception) as e:
            print('CarParts failed to create: ', e)
            car_parts_obj = None
            pass
        try:
            manufacturer_obj = await Manufacturer.objects.acreate(
                                        name=name
                                      )
        except (Manufacturer.DoesNotExist, ObjectDoesNotExist, 
                Exception) as e:
            print('Manufacturer failed to create: ', e)
            manufacturer_obj = None
            pass

        if car_parts_obj and manufacturer_obj:
            try:
                car_obj = await Car.objects.acreate(
                                  car_parts=car_parts_obj, 
                                  manufacturer=manufacturer_obj
                                )
                if car_obj:
                    try:
                        await car_obj.store.acreate(
                               location=location
                              )
                    except Exception as e:
                        print('Couldnt add store object 
                               relations for m2m: ', e)
            except (Car.DoesNotExist, ObjectDoesNotExist, 
                    Exception) as e:
                print('Car failed to create: ', e)
                pass

        end = time.time()
        print('Async POST time: ', end - start)
        return HttpResponse("ok", status=200)
Войти в полноэкранный режим

Выйти из полноэкранного режима

в get() метод, мы перебираем объекты в нашей модели автомобиля, и в цикле мы делаем один простой запрос и один немного сложный запрос с Q-выражениями. Все обернуто новым интерфейсом ORM, представленным в 4.1. В конце мы вызываем глобальную функцию, которая выполняет итерацию и возвращает набор запросов, элементы которого будут добавлены в пустой список. Между тем, в post() методом мы создаем некоторые объекты для наших моделей и обновляем существующие данные и, в конце концов, удаляем. Данные в почте получены от клиента через запрос. Удаление может быть самостоятельным методом, как и обновление, но чтобы не усложнять ситуацию, get и post добьются цели.

Вы заметите в асинхронном классе, что я не использовал .add в моем обновлении поля m2m. Это потому, что (к сожалению) я не смог найти его асинхронную адаптацию в документации, а Django рассматривает это как операцию синхронизации. Поэтому я сделал небольшой обходной путь, создав объект «Магазин» напрямую.

Результаты теста

Метод get() для 50 объектов (отображает среднее значение по трем тестам):

  • асинхронный: ~0,39832 с

  • синхронизация: ~0,29502 с

Метод post() для создания одного объекта для всех моделей:

  • асинхронный: ~0,07699 с

  • синхронизация: ~0,08299 с

Метод get() для 500 объектов:

  • асинхронный: ~ 3,67973 с

  • синхронизация: ~2,39427 с

Метод get() для 1500 объектов:

  • асинхронный: ~8,79098 с

  • синхронизация: ~6,26344 с

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

Тест B: связь через API
Для этого теста вам может понадобиться установить python httpx упаковка. Я сначала попробовал python requests пакет, полагая, что у него есть асинхронность, но, к сожалению, его не было, поэтому httpx пришел в качестве прекрасной альтернативы. Кроме того, встроенный в Python asyncio package также потребуется для сбора данных ожидаемого вызова функции. Наконец, мы импортируем shield из asyncio чтобы предотвратить возможную отмену.

pip install httpx
# Then in your view
import httpx
import asyncio
from asyncio import shield
Войти в полноэкранный режим

Выйти из полноэкранного режима

Используя запрос GET, мы свяжемся с внешним API для получения курсов обмена на apilayer.com.

Примечание: я использовал бесплатную подписку, которая действует как пробная/демонстрационная версия для целей тестирования, что идеально подходит для этого тестирования. В частности, я использовал API на https://apilayer.com/marketplace/exchangerates_data-api. Кроме того, я хочу отметить, что его довольно легко использовать, и он даже предлагает код шаблона для нескольких языков программирования для вызова своего API.

Из предлагаемого набора API мы используем только один, а именно:

GET/timeseries

Целевой API позволяет нам извлекать обменные курсы из прошлого (максимальный период 365 дней), что должно привести к длинному ответу, который должен занять некоторое время для сборки/отправки. apilayer.com действительно быстрый и отзывчивый, и по этой причине для этого теста берется целый год обменных курсов.

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

Представления на основе функций API:

def api_exchange_sync():
    url = "

    payload = {}
    headers = {
        "apikey": "[Insert your API key]"
    }

    r = httpx.get(url, params={"start_date": "2016-01-02", 
                               "end_date": "2017-01-01"},
                               headers=headers)

    return r.json()

async def api_exchange_async():
    url = "

    payload = {}
    headers = {
        "apikey": "[Insert your API key]"
    }

    async with httpx.AsyncClient(timeout=50.0) as client:
        r = await client.get(url, params={"start_date": "2016-01-
                             02", "end_date": "2017-01-01"},
                             headers=headers)

    return r.json()
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы устанавливаем тайм-аут на AsyncClient, чтобы предотвратить ошибку httpx.ReadTimeout. Это может произойти, если вы нажмете кнопку для перезапуска вызова API: P.

Синхронные и асинхронные представления на основе функций:

def append_data_sync(request):
    start = time.time()
    e_list = []
    data = [api_exchange_sync()]
    for k in data:
        e_list.append(k)
    end = time.time()
    print('read_sync time: ', end - start)
    return HttpResponse()

async def append_data_async(request):
    start = time.time()
    e_list = []
    data = await asyncio.gather(*[api_exchange_async()])
    for d in data:
        e_list.append(d)
    end = time.time()
    print('read_async time: ', end - start)
    return HttpResponse()
Войти в полноэкранный режим

Выйти из полноэкранного режима

Результаты теста

  • асинхронный: ~2,23437 с

  • синхронизация: ~3,30023 с

Когда мы перебираем списки полученных данных от функций API, мы добавляем их в список. Будущие объекты из asyncio.gather() оказалось выгодным, и я считаю, что если бы использовали потоковую передачу данных, мы были бы еще быстрее. Async выиграл гонку в этом тесте.


Вывод

Асинхронизация пригодилась, пока мы ждали ответа API, нам удалось быстрее получать данные и мы лучше использовали время. Синхронизация отлично себя зарекомендовала, когда возникла необходимость во внутренней обработке данных и связи с БД через ORM. Не было необходимости в ожидании, экранировании и многопоточности, поэтому появилась дополнительная скорость синхронизации. Нет необходимости сравнивать, что быстрее с синхронизацией и асинхронностью, потому что это сильно зависит от контекста того, что мы хотим сделать. Важно распознавать ситуации с привязкой к ЦП и вводу-выводу, чтобы правильно использовать асинхронность и синхронизацию. Нам не нужно использовать асинхронность для каждой небольшой задержки, с которой мы сталкиваемся. Я считаю, что система задержек (во время нашей разработки) должна быть замечена, прежде чем будет принято решение о реализации асинхронного подхода.

Асинхронное обновление Django 4.1 не является скоростным обновлением — я скорее рассматриваю его как обновление, которое позволяет нам лучше охватывать больше ситуаций.


использованная литература

  1. www.mendix.com, https://www.mendix.com/blog/asynchronous-vs-synchronous-programming/«Разница между асинхронным и синхронным программированием», Дэвид Беванс

  2. djangoproject.com, https://docs.djangoproject.com/«Асинхронные связанные страницы, связанные изменения 4.1 и сайты»

  3. djangoproject.com,

  4. djangoproject.com,