angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

FastAPI + SQLAlchemy: Асинхронный ввод-вывод и Back Pressure

Я хотел создать асинхронный SQLAlchemy API , но в итоге построил синхронный SQLAlchemy API.

4 июня 2021
post main image
pexels.com

APIs становятся все более важными. Компании хотят делиться своими данными с клиентами. Или хотят дать возможность третьим лицам создавать приложения на основе их APIs.

Несколько месяцев назад я создал API с Flask, SQLAlchemy, Marshmallow и APISpec, это было не очень сложно, и работает отлично. Затем я прочитал больше о FastAPI, API framework , который также поддерживает Python async из коробки. Он основан на Starlette, легком ASGI framework для построения высокопроизводительных asyncio сервисов.

Этот пост о том, почему я строю FastAPI синхронное SQLAlchemy демо API приложение, а не асинхронное SQLAlchemy API. Тема сложная. Ниже я процитирую некоторые тексты с сайта FastAPI и Github, чтобы не рассказать вам неправильную историю.

Если вам интересно, вы можете посмотреть демо-приложение здесь:

https://fastapifriends.peterspython.com

API с асинхронным доступом к базе данных

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

Асинхронные операции и Back Pressure

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

Armin Ronacher, создатель Flask web framework, написал интересную статью о Back Pressure, смотрите ссылки ниже, пожалуйста, прочитайте. В синхронном случае доступ к нашей базе данных блокируется, когда у нас заканчиваются сессии базы данных, новые запросы блокируются до тех пор, пока сессии снова не станут доступными.

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

Предотвращение проблем с Back Pressure

FastAPI Github issue #857 - @dmontagu написал:

Хотя было бы лучше, если бы существовал прямой способ обработки этого на уровне приложения, даже если у вас очень большая нагрузка, на практике вы обычно можете предотвратить проблемы Back Pressure , просто используя балансировщик нагрузки с ограничением скорости и хорошую политику автоматического масштабирования. Это не поможет во всех случаях и не спасет вас от особенно неудачного выбора дизайна, но для 99% развернутых python-приложений это уже будет излишеством.

Starlette Github issue #802 - @tomchristie написал:

Uvicorn в настоящее время позволяет вам установить --limit-concurrency <int>, который жестко ограничивает максимальное количество допустимых задач, которые могут быть запущены до возврата 503. В самом ограниченном случае вы можете просто установить это значение, основываясь на количестве доступных соединений с базой данных
. На самом деле, мы могли бы сделать лучше - было бы лучше, если бы Uvicorn ставил запросы в очередь сверх этого лимита, и отвечал 503 только после тайм-аута.

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

Эти типы компонентов всегда должны иметь некоторое ограничение пула и должны поднимать таймаут, если ресурс не может быть получен в течение определенного периода времени. Если Starlette настроен на отображение этих исключений на 503 ответ, то вы получите изящное поведение при отказе, когда сервер перегружен.

Синхронный и асинхронный SQLAlchemy

Я использую SQLAlchemy ORM там, где это возможно. SQLAlchemy 1.4 был выпущен недавно и имеет поддержку Python asyncio. Но есть и ограничения. Одно из них заключается в том, что вы не можете использовать ленивую загрузку, поскольку связанные объекты могут истечь и вызвать исключение asyncio . Выходом из этой ситуации является использование функции 'run_sync()', которая позволяет запускать синхронные функции под asyncio.

Pydantic's orm_mode

Со страницы FastAPI 'SQL (Relational) Databases':

Без orm_mode, если бы вы вернули модель SQLAlchemy из вашей операции пути, она не включала бы данные отношений. Даже если бы вы объявили эти отношения в своих моделях Pydantic . Но в режиме ORM , поскольку Pydantic сам будет пытаться получить доступ к нужным ему данным из атрибутов (вместо предположения о дикте), вы можете объявить конкретные данные, которые вы хотите вернуть, и он сможет пойти и получить их, даже из ORMs.

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

Я не использую асинхронный ввод-вывод (пока).

Короче говоря, асинхронный ввод-вывод создает дополнительные сложности и, в моем случае, производительность, вероятно, не улучшится. Есть много приложений, которые могут выиграть, но стандартный API , управляемый базой данных, не выиграет. Тогда почему все еще используется FastAPI? Основная причина для меня в том, что в ближайшем будущем я, возможно, захочу добавить больше функциональности, которая может выиграть от asyncio.

Синхронный SQLAlchemy: один объект сессии базы данных во время запроса

Я использую метод и код, описанный в разделе FastAPI 'Альтернативная сессия БД с промежуточным ПО': 'Промежуточное ПО, которое мы добавим (просто функция), будет создавать новый SQLAlchemy SessionLocal для каждого запроса, добавлять его в запрос и затем закрывать его после завершения запроса.'

Код показан в этом разделе:

@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
    response = Response("Internal server error", status_code=500)
    try:
        request.state.db = SessionLocal()
        response = await call_next(request)
    finally:
        request.state.db.close()
    return response


def get_db(request: Request):
    return request.state.db

Всем запросам присваивается объект сессии базы данных. Если мы когда-нибудь захотим добавить какую-нибудь асинхронную функцию (функции), мы можем исключить эти маршруты из db_session_middleware().

Зависимости, возвращающие объекты базы данных

Если вы используете синхронный SQLAlchemy и используете Dependency Injection , который возвращает объект базы данных, вы ДОЛЖНЫ использовать объект сессии базы данных, который действителен на протяжении всего запроса. В противном случае срок действия полученного объекта может истечь. Это звучит очевидно, но документация FastAPI не очень четко об этом говорит. Например, предположим, что у нас есть функция 'read_items()' с зависимостью get_current_user():

def get_current_user(
    db: Session = Depends(get_db)):
):
    # get  User  object by  user_id in JWT
    ...
    return db_user


@app.get("/items")
def read_items(
        db_user: schemas.User  = Depends(get_current_user)
        db: Session = Depends(get_db),
):
    # do something
    ...
    items = crud.get_items(db, db_user)
    return  users

Предположим, что мы не используем функцию db_session_middleware() , как указано выше, а вместо нее используем:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

В этом случае мы можем иметь несколько сессий базы данных во время запроса, и объект db_user может истечь в функции read_items() , потому что сессия базы данных закрывается после обработки get_current_user().

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

Резюме

Я хотел создать API на основе FastAPI и SQLAlchemy и PostgreSQL. Первое, с чем я столкнулся, был вопрос о том, использовать синхронный или асинхронный SQLAlchemy. Я уже знал, что асинхронные операции могут вызвать проблемы при неправильном обращении. Было познавательно прочитать, что другие говорят об асинхронном IO (и FastAPI). Чтобы избежать проблем, я решил сначала перейти к синхронному SQLAlchemy .

Ссылки / кредиты

Alternative DB session with middleware
https://fastapi.tiangolo.com/tutorial/sql-databases/

Asynchronous I/O (asyncio)
https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html

Asynchronous Python and Databases
https://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/

asyncio support for SQLAlchemy (and Flask, and any other blocking-IO library)
https://gist.github.com/zzzeek/2a8d94b03e46b8676a063a32f78140f1

Back pressure? #802
https://github.com/encode/starlette/issues/802#issuecomment-574606003

Backpressure explained — the resisted flow of data through software
https://medium.com/@jayphelps/backpressure-explained-the-flow-of-data-through-software-2350b3e77ce7

I'm not feeling the async pressure
https://lucumr.pocoo.org/2020/1/1/async-pressure/

Starlette
https://www.starlette.io/

Оставить комментарий

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

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.