Асинхронность

Дмитрий Шурмакин — ML Engineer

Преимущества асинхронного кода

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

Вот несколько преимуществ асинхронного кода в FastAPI

  1. Асинхронные запросы: Асинхронные запросы позволяют обрабатывать несколько запросов одновременно без блокировки, что может повысить пропускную способность вашего приложения.
  2. Эффективное использование ресурсов: Асинхронный код позволяет эффективнее использовать ресурсы сервера, так как он не блокирует поток в ожидании завершения операций ввода/вывода.
  3. Отзывчивость приложения: Асинхронный код может повысить отзывчивость вашего приложения, поскольку запросы могут быть обработаны независимо друг от друга, что позволяет избежать ожидания и ускорить обработку.
  4. Улучшенная масштабируемость: Многоядерные процессоры позволяют улучшить производительность, выполняя несколько операций одновременно, что особенно полезно для параллельных задач. Асинхронный код, в свою очередь, улучшает эффективность выполнения задач в однопоточном окружении, позволяя освобождать основной поток выполнения. В распределенных средах, асинхронность может быть использована для управления задачами между различными узлами, что увеличивает общую производительность и масштабируемость системы.

Асинхронность может быть полезной для улучшения производительности в приложениях, особенно когда задача требует ожидания на завершение операций ввода-вывода (I/O-bound), взаимодействие с базой данных или внешними API. Однако, для CPU-bound задач, где выполнение операций требует значительных вычислительных ресурсов, асинхронный подход менее эффективен из-за дополнительной сложности и накладных расходов на управление асинхронным кодом. В таких случаях, использование параллелизма или многозадачности может быть более подходящим решением для улучшения.

Когда не стоит применять асинхронный код

Предположим, мы выполняем вычисление произведения двух огромных матриц. Такие операции являются CPU-bound, потому что они требуют значительных вычислительных ресурсов и выполняются на процессоре. В данном случае, использование асинхронного кода не даст преимущества, так как нет возможности передать управление другим задачам, пока текущая задача не завершится. Вместо этого, для ускорения вычислений, можно использовать параллельные потоки или процессы, чтобы распределить нагрузку на несколько ядер процессора.

В Python, например, можно использовать модуль multiprocessing для создания процессов, которые могут выполняться параллельно и обрабатывать различные части матричного произведения одновременно. Это позволит более эффективно использовать вычислительные ресурсы и сократить общее время вычислений. Однако, стоит отметить, что в Python из-за существования GIL (Global Interpreter Lock), многопоточность не всегда может дать преимущества в производительности для CPU-bound задач. В таких случаях, использование процессов может быть более эффективным решением.

Примеры использования асинхронности в МЛ сервисах с FastAPI

  1. Асинхронный эндпоинт с тяжелыми вычислениями. Для вычислительных задач, которые занимают много времени на CPU (CPU-bound), асинхронность не даст прироста производительности. Однако, если вы комбинируете такие задачи с вводом-выводом, асинхронность может помочь оптимизировать общий процесс. Например, можно выполнить инференс в отдельном потоке, чтобы не блокировать основной поток выполнения.
  2. Асинхронный эндпоинт с запросами к базе данных. Если ваша модель требует данных из базы данных для инференса, асинхронный доступ к базе данных может улучшить производительность.
  3. Асинхронный эндпоинт с внешними API. Если ваша модель зависит от данных внешних API, асинхронные запросы помогут избежать блокировки выполнения.

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

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

Ниже представлен пример сервиса, который позволит оценить скорость работы в случае множества запросов.

from fastapi import FastAPI
from pydantic import BaseModel
import time
import asyncio

app = FastAPI()

class Item(BaseModel):
    item_id: int

def predict(input_data):
    time.sleep(1)  # Имитация работы модели
    return {"prediction": "класс A"}

async def predict_async(input_data):
    await asyncio.sleep(1)  # Имитация работы модели
    return {"prediction": "класс A"}

@app.post("/predict/")
def predict_endpoint(item_id: Item):
    result = predict(item_id.item_id)
    return result

@app.post("/predict_async/")
async def predict_endpoint_async(item_id: Item):
    result = await predict_async(item_id.item_id)
    return result

Результаты

Для корректного моделирования работы большого числа пользователей необходимо реализовать параллельные запросы к эндпоинтам. Однако для полноценного нагрузочного тестирования важно проводить тесты длительное время и использовать специализированные инструменты, такие как k6, Locust, Gatling, Hammer, Bombardier и другие. Эти инструменты позволяют более точно имитировать поведение пользователей и анализировать производительность системы под нагрузкой.

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

Таблица результатов тестирования

Тип сервиса Количество воркеров Количество запросов Процент ошибок Среднее время ответа (мс) Минимальное время ответа (мс) Максимальное время ответа (мс) Медианное время ответа (мс) Запросов в секунду Ошибок в секунду
Синхронный 1 2343 0.00% 2493 1122 3147 2200 39.27 0.00
Асинхронный 1 5822 0.00% 1014 1002 1184 1002 97.55 0.00
Синхронный 4 5811 0.00% 1018 1002 1188 1002 97.31 0.00
Асинхронный 4 5812 0.00% 1016 1002 1169 1002 97.32 0.00

Выводы

  1. Среднее время ответа и пропускная способность:
    • При использовании одного воркера асинхронный сервис значительно превосходит синхронный, обрабатывая в среднем запросы за 1014 мс против 2493 мс у синхронного сервиса. Асинхронный сервис обрабатывает запросы более чем в 2 раза быстрее.
    • Количество запросов в секунду также значительно выше у асинхронного сервиса: 97.55 rps против 39.27 rps у синхронного сервиса.
  2. Использование 4 воркеров:
    • При увеличении числа воркеров до 4 разница в производительности между синхронным и асинхронным сервисами становится минимальной. Среднее время ответа для обоих сервисов составляет около 1016 мс, и количество запросов в секунду почти идентично: 97.31 rps для синхронного сервиса и 97.32 rps для асинхронного сервиса.
  3. Процент ошибок:
    • В обоих случаях (с одним и четырьмя воркерами) ни один из сервисов не показал ошибок, что свидетельствует о стабильной работе при данных нагрузках.
  4. Заключение:
    • Асинхронный сервис значительно эффективнее синхронного при использовании одного воркера, что делает его предпочтительным для систем с ограниченными ресурсами или когда масштабируемость по воркерам затруднена.
    • При увеличении числа воркеров до 4 производительность обоих сервисов выравнивается, что указывает на то, что синхронные сервисы могут быть также эффективны в условиях параллелизма.