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

FastAPI + SQLAlchemy: Asynchrone IO und Back Pressure

Ich wollte einen asynchronen SQLAlchemy API , habe aber letztendlich einen synchronen SQLAlchemy API gebaut.

4 Juni 2021
post main image
pexels.com

APIs gewinnen immer mehr an Bedeutung. Unternehmen wollen ihre Daten mit Kunden teilen. Oder sie wollen Dritten ermöglichen, Anwendungen auf Basis ihrer APIs zu erstellen.

Vor ein paar Monaten habe ich eine API mit Flask, SQLAlchemy, Marshmallow und APISpec erstellt, es war nicht wirklich schwierig und funktioniert gut. Dann las ich mehr über FastAPI, einen API , der auch Python async out of the box unterstützt. Es basiert auf Starlette, einem leichtgewichtigen ASGI framework zum Aufbau von hochperformanten asyncio -Diensten.

In diesem Beitrag geht es darum, warum ich eine FastAPI synchrone SQLAlchemy Demo API Anwendung baue und nicht eine asynchrone SQLAlchemy API. Das Thema ist komplex. Im Folgenden zitiere ich einige Texte von der FastAPI -Website und von Github, um zu vermeiden, dass ich Ihnen etwas Falsches erzähle.

Falls Sie interessiert sind, können Sie die Demo-Anwendung hier sehen:

https://fastapifriends.peterspython.com

API mit asynchronem Datenbankzugriff

Im Allgemeinen sind asynchrone Anfragen nicht schneller als synchrone, sie tun das Gleiche. Bei einem synchronen Request wird die Ausführung des Aufrufers angehalten und man muss auf die Fertigstellung der Aufgabe warten. Bei asynchronen Anfragen wird die Anfrage in eine Warteschlange gestellt und erst einige Zeit später bearbeitet, Sie warten nicht auf das Ergebnis, sondern können andere Dinge tun, während Sie auf den Abschluss der Aufgabe warten.

Asynchroner Betrieb und Back Pressure

Mit asynchronen Operationen bin ich immer sehr vorsichtig gewesen. Ein triviales Beispiel ist eine Webseite mit einer Schaltfläche, die einen AJAX -Aufruf verwendet. Ich habe eine neue Anforderung erst dann zugelassen, wenn die vorherige Anforderung beendet war. Sehr konservativ, aber ich möchte einfach Probleme vermeiden.

Armin Ronacher, der Schöpfer des Flask web framework, hat einen interessanten Artikel über Back Pressure geschrieben, siehe Links unten, bitte lesen. Im synchronen Fall wird der Zugriff auf unsere Datenbank blockiert, wenn wir keine Datenbanksitzungen mehr haben, neue Anfragen werden blockiert, bis wieder Sitzungen verfügbar sind.

Im asynchronen Fall werden alle Anfragen in eine Warteschlange gestellt, die darauf wartet, bearbeitet zu werden. Wenn die eingehende Rate höher ist als die Fähigkeit des Systems, sie zu verarbeiten, wird dem Server der Speicher ausgehen und er wird abstürzen. Sie können dies verhindern, indem Sie sich die maximale Anzahl von Anfragen ansehen, die in die Warteschlange gestellt werden können. Das klingt einfach, ist es aber nicht.

Verhindern von Back Pressure -Problemen

FastAPI Github issue #857 - @dmontagu schrieb:

Obwohl es besser wäre, wenn es eine einfache Möglichkeit gäbe, dies in der Anwendungsschicht zu handhaben, selbst wenn Sie eine sehr hohe Last haben, können Sie in der Praxis in der Regel Back Pressure -Probleme verhindern, indem Sie einfach einen ratenbegrenzenden Load Balancer und eine gute Auto-Skalierungsrichtlinie verwenden. Das wird nicht alle Fälle abdecken, und wird Sie nicht vor besonders schlechten Design-Entscheidungen bewahren, aber für 99% der eingesetzten Python-Anwendungen wäre das schon ein Overkill.

Starlette Github issue #802 - @tomchristie schrieb:

Uvicorn erlaubt es Ihnen derzeit, ein --limit-concurrency <int> zu setzen, das die maximale Anzahl der zulässigen Tasks, die laufen dürfen, bevor 503's zurückgegeben werden, hart begrenzt. Im einfachsten Fall könnten Sie dies einfach auf der Basis der Anzahl der verfügbaren Datenbankverbindungen
festlegen. Eigentlich könnten wir es da besser machen - es wäre schöner, wenn Uvicorn Anfragen, die über dieses Limit hinausgehen, in eine Warteschlange stellen und erst nach einer Timeout-Periode mit einer 503 antworten würde.

Die andere Sache, die wir tun könnten, die schlau wäre, ist die Vorgabe von Timeout-Ausnahmen bei 503-Antworten. Also... angenommen, Sie haben ein Ressourcenlimit für: das Erfassen von Datenbankverbindungen, die Verbindung zum Cache, das Senden ausgehender HTTP-Anfragen, das Senden von E-Mails.

Diese Arten von Komponenten sollten immer eine Art von Pool-Limitierung zur Verfügung haben und sollten Timeouts auslösen, wenn die Ressource für eine gewisse Zeit nicht erfasst werden kann. Wenn Starlette so konfiguriert ist, dass diese Ausnahmen auf 503-Antworten abgebildet werden, dann erhalten Sie ein anmutiges Fehlerverhalten, wenn ein Server überlastet ist.'

Synchrones vs. asynchrones SQLAlchemy

Ich verwende SQLAlchemy ORM wo immer möglich. SQLAlchemy 1.4 wurde kürzlich veröffentlicht und hat Unterstützung für Python asyncio. Aber es gibt Einschränkungen. Eine davon ist, dass Sie das faule Laden nicht verwenden können, weil verwandte Objekte ablaufen können und eine asyncio -Ausnahme auslösen könnten. Ein Ausweg ist die Verwendung der Funktion 'run_sync()', die es ermöglicht, synchrone Funktionen unter asyncio auszuführen.

Pydantic's orm_mode

Von der Seite FastAPI 'SQL (Relational) Databases':

Wenn Sie ohne orm_mode ein SQLAlchemy -Modell aus Ihrer Pfad-Operation zurückgeben, würde es die Beziehungsdaten nicht enthalten. Selbst wenn Sie diese Beziehungen in Ihren Pydantic -Modellen deklariert hätten. Da Pydantic selbst versucht, auf die benötigten Daten aus Attributen zuzugreifen (anstatt ein Diktat anzunehmen), können Sie im ORM -Modus die spezifischen Daten deklarieren, die Sie zurückgeben möchten, und es wird in der Lage sein, sie zu holen, sogar aus ORMs.

Das würde bedeuten, dass Sie, wenn Sie ein Pydantic -Modell für die Antwort verwenden, auch die Beziehungsdaten erhalten. Ich weiß nicht, ob dies immer zuverlässig die Beziehungsdaten erhält, muss dies ausprobieren.

Ich verwende (noch) kein asynchrones IO

Kurz gesagt, async IO führt zusätzliche Komplexität ein und in meinem Fall wird sich die Leistung wahrscheinlich nicht verbessern. Es gibt viele Anwendungen, die davon profitieren können, aber ein standardmäßiger datenbankgesteuerter API tut das nicht. Warum dann noch FastAPI verwenden? Der Hauptgrund für mich ist, dass ich in naher Zukunft vielleicht mehr Funktionalität hinzufügen möchte, die von asyncio profitieren kann.

Synchroner SQLAlchemy: ein Datenbanksitzungsobjekt während einer Anfrage

Ich verwende die Methode und den Code, die im Abschnitt FastAPI "Alternative DB-Sitzung mit Middleware" beschrieben sind: "Die Middleware, die wir hinzufügen werden (nur eine Funktion), wird für jede Anfrage eine neue SQLAlchemy SessionLocal erstellen, sie der Anfrage hinzufügen und sie dann schließen, sobald die Anfrage beendet ist.

Der Code ist in diesem Abschnitt dargestellt:

@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

Allen Anfragen wird ein Datenbank-Session-Objekt zugewiesen. Wenn wir jemals einige asynchrone Funktion(en) hinzufügen wollen, können wir diese Routen aus db_session_middleware() ausschließen.

Abhängigkeiten, die Datenbankobjekte zurückgeben

Wenn Sie den synchronen SQLAlchemy verwenden und Dependency Injection verwenden, der ein Datenbankobjekt zurückgibt, MÜSSEN Sie ein Datenbanksitzungsobjekt verwenden, das die ganze Anfrage über gültig ist. Andernfalls kann das abgerufene Objekt verfallen. Das klingt offensichtlich, aber die FastAPI -Dokumentation ist diesbezüglich nicht sehr klar. Nehmen wir zum Beispiel an, wir haben eine Funktion 'read_items()' mit einer Abhängigkeit 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

Nehmen wir an, wir verwenden nicht die Funktion " db_session_middleware() " wie oben erwähnt, sondern würden stattdessen die Funktion "read_items()" verwenden:

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

In diesem Fall können wir während einer Anfrage mehrere Datenbanksitzungen haben und das Objekt db_user kann in der Funktion read_items() ablaufen, da die Datenbanksitzung nach der Verarbeitung von get_current_user() geschlossen wird.

Wenn wir asynchron SQLAlchemy verwenden würden, wird das zurückgegebene db_user -Objekt von der Sitzung gelöst und bleibt verfügbar. Das ist zumindest das, was ich erwarten würde.

Zusammenfassung

Ich wollte eine API erstellen, die auf FastAPI und SQLAlchemy und PostgreSQL basiert. Das erste, womit ich konfrontiert wurde, war die Frage, ob ich synchrones oder asynchrones SQLAlchemy verwenden sollte. Ich wusste bereits, dass asynchrone Operationen Probleme verursachen können, wenn sie nicht richtig gehandhabt werden. Es war informativ zu lesen, was andere über asynchrone IO (und FastAPI) sagten. Um Probleme zu vermeiden, entschied ich mich zunächst für die synchrone SQLAlchemy .

Links / Impressum

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/

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.