DevGang
Авторизоваться

Анализ производительности промежуточного ПО FastAPI

FastAPI - это известный и любимый (почти 70 тысяч звезд на GitHub) современный, быстрый, асинхронный веб-фреймворк для создания API на Python. Он создан для простоты использования и высокой производительности, но при этом надежен и готов к производству.

Моей команде очень понравился процесс разработки приложений с помощью FastAPI, и она нашла в нем полезный опыт обучения. Однако один из моментов оказался немного сложным: когда дело дошло до тестирования производительности, мы столкнулись с неожиданно низкими значениями запросов в секунду (RPS), что побудило нас к более глубокому исследованию, которое, в свою очередь, привело нас к более подробному изучению FastAPI Middlewares.

Цель этой заметки - изучить влияние BaseHTTPMiddleware и ASGIMiddleware на производительность FastAPI-приложений и поделиться нашими выводами.

Спойлер: вот как может измениться производительность реального производственного приложения в зависимости от типа промежуточного ПО, которое мы используем для реализации некоторой общей логики:

Справочная информация

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

Если вы хотите применить общую логику для обработки запросов в нескольких конечных точках, вы можете использовать промежуточные модули. FastAPI (точнее, Starlette, на котором базируется FastAPI) предлагает два типа промежуточного ПО:

  • BaseHTTPMiddleware: Этот тип промежуточного ПО создан на основе простого и понятного интерфейса. При использовании BaseHTTPMiddleware вы определяете асинхронную функцию, которая принимает запрос, вызывает следующий элемент в цепочке промежуточного ПО, а затем вы можете изменить ответ.
  • ASGIMiddleware: ASGI (Асинхронный серверный шлюз cпецификациb интерфейса) гласит: Можно иметь "промежуточное ПО" ASGI - код, который играет роль и сервера, и приложения, принимая область видимости и ожидаемые вызовы отправки/получения, потенциально изменяя их, а затем вызывая внутреннее приложение. ASGIMiddleware функционирует в рамках ASGI-приложения, которое является основным компонентом таких фреймворков, как FastAPI и Starlette. Более того, такое промежуточное ПО разделяет с ASGI-приложениями одну и ту же сигнатуру coroutine application(scope, receive, send). В целом, реализация и поддержка ASGIMiddleware сложнее, чем BaseHTTPMiddleware, из-за более низкого уровня абстракции, предлагающего более прямое взаимодействие с протоколом ASGI. Важно отметить, что ASGIMiddleware не подвержено некоторым важным ограничениям BaseHTTPMiddleware.

Поскольку промежуточные модули выступают в качестве прослойки в жизненном цикле "запрос-ответ", ожидается, что они будут влиять на производительность приложения. Вопрос в том, насколько сильно? И в чем разница между двумя типами промежуточных модулей, поддерживаемых в FastAPI?

Процесс выявления

Во время нагрузочного тестирования производительности с помощью ApacheBench (ab) мы обнаружили, что, хотя приложение может обрабатывать требуемый RPS, он всё еще кажется довольно низким для асинхронного приложения, которое не содержит никаких значительных вызовов, привязанных к процессору. Еще одним поводом для беспокойства стало то, что RPS не масштабируется линейно с количеством экземпляров приложения. Мы решили изучить этот вопрос подробнее.

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

Эксперименты и анализ

Здесь представлены пять экспериментов, которые хорошо отражают процесс исследования, через который прошла наша команда. Я буду выполнять запросы ApacheBench (ab) в каждом экспериментальном случае с параллелизмом 100, чтобы измерить RPS нашего приложения с различными промежуточными продуктами. Я буду запускать каждый случай по пять раз и усреднять результаты.

>> ab -c 100 -n 1000 http://127.0.0.1:8000/

Настройка эксперимента

Пример 0: базовый уровень (без промежуточных устройств)

Начнем с простого приложения FastAPI, содержащего одну конечную точку и не имеющего промежуточного программного обеспечения.

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}

Пример 1: одно простое среднее программное обеспечение

Здесь представлены реализации обоих типов промежуточных программ. Оба они фактически ничего не делают.

# BaseHTTPMiddleware
from urllib.request import Request
from starlette.middleware.base import BaseHTTPMiddleware


class HTTPMiddleware:
    async def __call__(self, request: Request, call_next):
        return await call_next(request)


app.add_middleware(BaseHTTPMiddleware, dispatch=HTTPMiddleware())

# ASGIMiddleware
from starlette.types import ASGIApp, Send, Scope, Receive


class ASGIMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        await self.app(scope, receive, send)


app.add_middleware(ASGIMiddleware)

Пример 2: пять простых, объединенных в стек программных обеспечений

Программные обеспечения по-прежнему ни на что не влияют, но можно попробовать сложить их в один стек, чтобы узнать, что будет!

# BaseHTTPMiddleware
for _ in range(5):
    app.add_middleware(BaseHTTPMiddleware, dispatch=HTTPMiddleware())

# ASGIMiddleware
for _ in range(5):
    app.add_middleware(ASGIMiddleware)

Пример 3: Пять стекированных блокирующих ПО

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

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

# BaseHTTPMiddleware
class HTTPMiddleware:
    async def __call__(self, request: Request, call_next):
        with open('/tmp/middleware.log', 'a') as f:
            f.write('HTTPMiddleware\n' * 1000)
        return await call_next(request)


# ASGIMiddleware
class ASGIMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        with open('/tmp/middleware.log', 'a') as f:
            f.write('ASGIMiddleware\n' * 1000)
        await self.app(scope, receive, send)

Пример 4: Пять стекированных неблокирующих ПО

Давайте посмотрим, влияет ли добавление асинхронных вызовов в наши промежуточные модули. Здесь я включаю вызов asyncio.sleep for 10ms в нашу логику.

# BaseHTTPMiddleware
class HTTPMiddleware:
    async def __call__(self, request: Request, call_next):
        await asyncio.sleep(0.01)
        return await call_next(request)


# ASGIMiddleware
class ASGIMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        await asyncio.sleep(0.01)
        await self.app(scope, receive, send)

Пример 5: Пять стекированных неблокирующих ПО с 100 ms sleep

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

Результаты и наблюдения

Вот краткое изложение результатов экспериментов, описанных выше.

Пример 1: одно простое программное обеспечение

  • Эффект от добавления одного промежуточного ПО довольно заметен, что говорит нам о заметных накладных расходах на работу промежуточного ПО.
  • BaseHTTPMiddleware работает на ~15% медленнее.

Пример 2: пять простых, объединенных в стек программных обеспечений

  • Влияние выполнения пяти BaseHTTPMiddlewares значительно. RPS падает на порядок по сравнению с Baseline.
  • ASGIMiddleware практически не страдает от суммирования.

Пример 3: Пять стекированных блокирующих ПО

  • Тяжелый блокирующий вызов не оказывает большого влияния на производительность BaseHTTPMiddleware, что говорит нам о том, что время ответа, вероятно, уже насыщено накладными расходами промежуточного ПО.
  • Влияние на ASGIMiddleware значительно, что вполне ожидаемо.

Пример 4: Пять стекированных неблокирующих ПО

  • BaseHTTPMiddleware с sleep работает чуть медленнее, чем в случае 2, где ПО промежуточного слоя ничего не делает, что в некоторой степени подтверждает идею о том, что мы максимально снизили влияние накладных расходов ПО промежуточного слоя.
  • ASGIMiddleware по-прежнему быстрее, но в меньшей степени.

Пример 5: Пять стекированных неблокирующих ПО с 100 ms sleep

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

Выводы и рекомендации

По результатам экспериментов BaseHTTPMiddleware имеет ощутимый прирост производительности по сравнению с ASGIMiddleware. Чем меньше время обработки логики промежуточного ПО, тем более существенной будет разница в производительности между двумя типами промежуточного ПО.

По всей видимости, это происходит из-за рутин управления задачами anyio, используемых Starlette для работы с промежуточными программами.

Класс BaseHTTPMiddleware предоставляет пользователю хорошую и простую абстракцию. Предоставляя непосредственный доступ к запросу, он делает разработку и поддержку промежуточного ПО легкой и экономически эффективной. В то же время за абстракцию приходится платить накладными расходами (выполнение задач, управление исключениями и потоками через библиотеку anyio).

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

Вот как выглядит эффект от перехода с BaseHTTPMiddleware на ASGIMiddleware в реальной жизни (оптимизированные промежуточные модули отмечены стрелками) - 20-30 % улучшение времени обработки запроса в одной конечной точке.

Даже выигрыш в производительности выглядит впечатляюще, как это часто бывает - это компромисс со сложностью. Если только вы не полагаетесь на множество стековых промежуточных модулей для своей бизнес-логики, я бы не советовал использовать ASGIMiddleware раньше, чем вас заставят это сделать требования к RPS и/или времени обработки запросов. Но важно держать эту опцию на вооружении и использовать ее, когда другие, более очевидные возможности оптимизации будут исчерпаны.

Заключение

Промежуточные модули FastAPI - это мощный инструмент для реализации различных функций, таких как аутентификация, обработка CORS, ведение логов, добавление пользовательских метрик и многое другое. Однако накладные расходы на выполнение промежуточного ПО могут быть значительными и должны учитываться при определении размера приложения. Я настоятельно рекомендую избегать преждевременной оптимизации и сохранять дизайн как можно более простым, что поможет увеличить время выхода на рынок и снизить затраты на обслуживание. Если же требования вынуждают вас к оптимизации, то переход на ASGIMiddleware может стать хорошим вариантом для рассмотрения.

Источник:

#Python #FastAPI
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу