[FIXED] FastAPI ist sehr langsam bei der Rückgabe einer großen Menge an JSON-Daten

Ausgabe

Ich habe einen FastAPI- GETEndpunkt, der eine große Menge an JSON-Daten zurückgibt (~ 160.000 Zeilen und 45 Spalten). Es überrascht nicht, dass es extrem langsam ist, die Daten mit json.dumps(). Ich lese zuerst die Daten aus einer Datei mit json.loads()und filtere sie nach den eingegebenen Parametern. Gibt es eine schnellere Möglichkeit, die Daten an den Benutzer zurückzugeben, als die Verwendung von return data? Im aktuellen Zustand dauert es fast eine Minute.

Mein Code sieht derzeit so aus:

# helper function to parse parquet file (where data is stored)
def parse_parquet(file_path):
    df = pd.read_parquet(file_path)
    result = df.to_json(orient = 'records')
    parsed = json.loads(result)
    return parsed
    

@app.get('/endpoint')
# has several more parameters
async def some_function(year = int | None = None, id = str | None = None):
    if year is None:
        data = parse_parquet(f'path/{year}_data.parquet')
    # no year
    if year is not None:
        data = parse_parquet(f'path/all_data.parquet')
    if id is not None:
        data = [d for d in data if d['id'] == id]
    return data

Lösung

Einer der Gründe für die langsame Antwort ist, dass Sie in Ihrer parse_parquet()Methode die Datei zunächst in JSON konvertieren (mit df.to_json()), dann in ein Wörterbuch (mit json.loads()) und schließlich wieder in JSON, da FastAPI im Hintergrund automatisch die Rückgabe konvertiert -Wert in JSON mithilfe von jsonable_encoder(was ziemlich langsam ist, selbst im Vergleich zur normalen JSON-Bibliothek, dh mit json.dumps()).

Diese Antwort von @MatsLindh im Kommentarbereich schlägt vor, alternative JSON-Encoder wie orjson oder ujosn zu verwenden , die den Prozess tatsächlich beschleunigen würden, verglichen mit der Verwendung der Standardeinstellung jsonable_encoderfür die Konvertierung von dictin JSON. Die Verwendung von Pandas to_json()und die direkte Rückgabe eines benutzerdefinierten Befehls Response– wie in Option 1 ( Update 2 ) dieser Antwort beschrieben – scheint jedoch die leistungsstärkste Lösung zu sein. Sie können den unten angegebenen Code verwenden, der eine benutzerdefinierte APIRouteKlasse verwendet , um die Antwortzeit für alle verfügbaren Lösungen zu vergleichen.

Verwenden Sie Ihre eigene Parkettdatei oder den folgenden Code, um eine Beispielparkettdatei zu erstellen, die aus 160.000 Zeilen und 45 Spalten besteht.

create_parquet.py

import pandas as pd
import numpy as np

columns = ['C' + str(i) for i in range(1, 46)]
df = pd.DataFrame(data=np.random.randint(99999, 99999999, size=(160000,45)),columns=columns)
df.to_parquet('data.parquet')

Führen Sie die FastAPI-App unten aus und greifen Sie separat auf jeden Endpunkt zu, um die Zeit zu überprüfen, die zum Abschluss des Ladevorgangs und der Konvertierung der Daten in JSON benötigt wird.

app.py

from fastapi import FastAPI, APIRouter, Response, Request
from fastapi.routing import APIRoute
from typing import Callable
import pandas as pd
import json
import time
import ujson
import orjson


class TimedRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            before = time.time()
            response: Response = await original_route_handler(request)
            duration = time.time() - before
            response.headers["Response-Time"] = str(duration)
            print(f"route duration: {duration}")
            return response

        return custom_route_handler

app = FastAPI()
router = APIRouter(route_class=TimedRoute)

@router.get("/defaultFastAPIencoder")
def get_data_default():
    df = pd.read_parquet('data.parquet')   
    return df.to_dict(orient="records")
    
@router.get("/orjson")
def get_data_orjson():
    df = pd.read_parquet('data.parquet')
    return Response(orjson.dumps(df.to_dict(orient='records')), media_type="application/json")

@router.get("/ujson")
def get_data_ujson():
    df = pd.read_parquet('data.parquet')   
    return Response(ujson.dumps(df.to_dict(orient='records')), media_type="application/json")

# Preferable way to do it  
@router.get("/pandasJSON")
def get_data_pandasJSON():
    df = pd.read_parquet('data.parquet')   
    return Response(df.to_json(orient="records"), media_type="application/json")  

app.include_router(router)

Even though the response time is quite fast using /pandasJSON above (and this is the preferable way to go), you may encounter some delay on displaying the data on the browser. That, however, has nothing to do with the server side, but with the client side, as the browser is trying to display a large amount of data. If you don’t want to display the data, but instead let the user download the data to their device (which would be much faster), you can set the Content-Disposition header to the Response using the attachment parameter and passing a filename as well, indicating to the browser that the file should be downloaded. For more details, have a look at this answer and this answer.

@router.get("/download")
def get_data():
    df = pd.read_parquet('data.parquet')
    headers = {'Content-Disposition': 'attachment; filename="data.json"'}
    return Response(df.to_json(orient="records"), headers=headers, media_type='application/json')

Ich sollte auch erwähnen, dass es eine Bibliothek namens Daskgibt, die große Datensätze verarbeiten kann, wie hier beschrieben , falls Sie eine große Menge an Datensätzen verarbeiten müssen und der Vorgang zu lange dauert. Ähnlich wie bei Pandas können Sie die .read_parquet()Methode zum Lesen der Datei verwenden. Da Dask anscheinend keine äquivalente .to_json()Methode bereitstellt, können Sie den Dask DataFrame mithilfe von in Pandas DataFrame konvertieren df.compute()und dann Pandas verwenden df.to_json(), um den DataFrame in eine JSON-Zeichenfolge zu konvertieren und ihn wie oben gezeigt zurückzugeben.


Beantwortet von –
Chris


Antwort geprüft von –
Marilyn (FixError Volunteer)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like