FastAPI UploadFile es lento en comparación con Flask

Resuelto user3656746 asked hace 3 años • 0 respuestas

He creado un punto final, como se muestra a continuación:

@app.post("/report/upload")
def create_upload_files(files: UploadFile = File(...)):
        try:
            with open(files.filename,'wb+') as wf:
                wf.write(file.file.read())
                wf.close()
        except Exception as e:
            return {"error": e.__str__()}

Se lanza con uvicorn:

../venv/bin/uvicorn test_upload:app --host=0.0.0.0 --port=5000 --reload

Estoy realizando algunas pruebas para cargar un archivo de alrededor de 100 MB usando solicitudes de Python y me lleva alrededor de 128 segundos:

f = open(sys.argv[1],"rb").read()
hex_convert = binascii.hexlify(f)
items = {"files": hex_convert.decode()}
start = time.time()
r = requests.post("http://192.168.0.90:5000/report/upload",files=items)
end = time.time() - start
print(end)

Probé el mismo script de carga con un punto final API usando Flask y tardó alrededor de 0,5 segundos:

from flask import Flask, render_template, request
app = Flask(__name__)


@app.route('/uploader', methods = ['GET', 'POST'])
def upload_file():
   if request.method == 'POST':
      f = request.files['file']
      f.save(f.filename)
      return 'file uploaded successfully'

if __name__ == '__main__':
    app.run(host="192.168.0.90",port=9000)

¿Hay algo que estoy haciendo mal?

user3656746 avatar Dec 17 '20 21:12 user3656746
Aceptado

Puede escribir los archivos usando escritura sincrónica , después de definir el punto final con def, como se muestra en esta respuesta , o usando escritura asincrónica (utilizando aiofiles ), después de definir el punto final con async def; UploadFileLos métodos son asyncmétodos y, por lo tanto, los necesitas await. A continuación se proporciona un ejemplo. Para obtener más detalles sobre defvs async defy cómo pueden afectar el rendimiento de su API (dependiendo de las tareas realizadas dentro de los puntos finales), consulte esta respuesta .

Cargar un solo archivo

aplicación.py

from fastapi import File, UploadFile
import aiofiles

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    try:
        contents = await file.read()
        async with aiofiles.open(file.filename, 'wb') as f:
            await f.write(contents)
    except Exception:
        return {"message": "There was an error uploading the file"}
    finally:
        await file.close()

    return {"message": f"Successfuly uploaded {file.filename}"}
Leer el archivo en trozos

Como se explica en esta respuesta , FastAPI/Starlette, en el fondo, utiliza un SpooledTemporaryFile que tiene el max_sizeatributo establecido en 1 MB, lo que significa que los datos se almacenan en la cola de impresión en la memoria hasta que el tamaño del archivo supera 1 MB, momento en el cual los datos se escriben en un archivo temporal en el disco y, por lo tanto, la llamada await file.read()en realidad leería los datos del disco en la memoria (si el archivo cargado tuviera más de 1 MB). Por lo tanto, es posible que desee utilizarlo asyncde forma fragmentada para evitar cargar el archivo completo en la memoria, lo que podría causar problemas; si, por ejemplo, tiene 8 GB de RAM, no puede cargar un archivo de 50 GB (sin mencionar que el La RAM disponible siempre será menor que la cantidad total instalada, ya que el sistema operativo nativo y otras aplicaciones que se ejecutan en su máquina usarán parte de la RAM). Por lo tanto, en ese caso, debería cargar el archivo en la memoria en fragmentos y procesar los datos un fragmento a la vez. Sin embargo, este método puede tardar más en completarse, dependiendo del tamaño del fragmento que elija; a continuación, es decir, 1024 * 1024bytes (= 1 MB). Puede ajustar el tamaño del trozo como desee.

from fastapi import File, UploadFile
import aiofiles

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    try:
        async with aiofiles.open(file.filename, 'wb') as f:
            while contents := await file.read(1024 * 1024):
                await f.write(contents)
    except Exception:
        return {"message": "There was an error uploading the file"}
    finally:
        await file.close()

    return {"message": f"Successfuly uploaded {file.filename}"}

Alternativamente, puede usar shutil.copyfileobj(), que se usa para copiar el contenido de un file-likeobjeto a otro file-likeobjeto (consulte también esta respuesta ). De forma predeterminada, los datos se leen en fragmentos con un tamaño de búfer (fragmento) predeterminado de 1 MB (es decir, 1024 * 1024bytes) para Windows y 64 KB para otras plataformas (consulte el código fuente aquí ). Puede especificar el tamaño del búfer pasando el lengthparámetro opcional. Nota: Si lengthse pasa un valor negativo, se leerá todo el contenido del archivo; consulte f.read()también la documentación, que .copyfileobj()se utiliza bajo el capó. El código fuente se .copyfileobj()puede encontrar aquí ; en realidad no hay nada tan diferente del enfoque anterior en la lectura/escritura del contenido del archivo. Sin embargo , .copyfileobj()utiliza operaciones de E/S de bloqueo detrás de escena, y esto resultaría en el bloqueo de todo el servidor (si se usa dentro de un async defpunto final). Por lo tanto, para evitar eso, puede usar Starlette run_in_threadpool()para ejecutar todas las funciones necesarias en un hilo separado (que luego se espera) para garantizar que el hilo principal (donde se ejecutan las rutinas) no se bloquee. FastAPI utiliza exactamente la misma función internamente cuando llama a los asyncmétodos del UploadFileobjeto, es decir, .write(), .read(). close(), etc.—consulte el código fuente aquí . Ejemplo:

from fastapi import File, UploadFile
from fastapi.concurrency import run_in_threadpool
import shutil
        
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    try:
        f = await run_in_threadpool(open, file.filename, 'wb')
        await run_in_threadpool(shutil.copyfileobj, file.file, f)
    except Exception:
        return {"message": "There was an error uploading the file"}
    finally:
        if 'f' in locals(): await run_in_threadpool(f.close)
        await file.close()

    return {"message": f"Successfuly uploaded {file.filename}"}

prueba.py

import requests

url = 'http://127.0.0.1:8000/upload'
file = {'file': open('images/1.png', 'rb')}
r = requests.post(url=url, files=file) 
print(r.json())

Para ver un <form>ejemplo de HTML, consulte aquí .

Cargar varios archivos

aplicación.py

from fastapi import File, UploadFile
import aiofiles

@app.post("/upload")
async def upload(files: List[UploadFile] = File(...)):
    for file in files:
        try:
            contents = await file.read()
            async with aiofiles.open(file.filename, 'wb') as f:
                await f.write(contents)
        except Exception:
            return {"message": "There was an error uploading the file(s)"}
        finally:
            await file.close()

    return {"message": f"Successfuly uploaded {[file.filename for file in files]}"}  
Leer los archivos en trozos

Para leer los archivos en fragmentos, consulte los enfoques descritos anteriormente en esta respuesta.

prueba.py

import requests

url = 'http://127.0.0.1:8000/upload'
files = [('files', open('images/1.png', 'rb')), ('files', open('images/2.png', 'rb'))]
r = requests.post(url=url, files=files) 
print(r.json())

Para ver un <form>ejemplo de HTML, consulte aquí .

Actualizar

Profundizando en el código fuente, parece que las últimas versiones de Starlette (que FastAPI usa debajo) usan SpooledTemporaryFile(para UploadFilela estructura de datos) con un max_sizeatributo establecido en 1 MB ( 1024 * 1024bytes), consulte aquí , en contraste con las versiones anteriores donde max_sizeestaba configurado en el valor predeterminado. valor, es decir, 0 bytes, como el que se muestra aquí .

Lo anterior significa que, en el pasado, los datos solían cargarse completamente en la memoria independientemente del tamaño del archivo (lo que podría generar problemas si un archivo no cabía en la RAM), mientras que, en la última versión, los datos se almacenan en cola. memoria hasta que el filetamaño exceda max_size(es decir, 1 MB), momento en el cual el contenido se escribe en el disco; más específicamente, al directorio temporal del sistema operativo (Nota: esto también significa que el tamaño máximo de archivo que puede cargar está limitado por el almacenamiento disponible en el directorio temporal del sistema. Si hay suficiente almacenamiento (para sus necesidades) disponible en su sistema, no hay nada de qué preocuparse; de ​​lo contrario, consulte esta respuesta sobre cómo cambiar el directorio temporal predeterminado). Por lo tanto, el proceso de escribir el archivo varias veces, es decir, cargar inicialmente los datos en la RAM, luego, si los datos exceden 1 MB de tamaño, escribir el archivo en un directorio temporal, luego leer el archivo desde el directorio temporal (usando file.read()) y finalmente , escribir el archivo en un directorio permanente, es lo que hace que la carga del archivo sea lenta en comparación con el uso del marco Flask, como señaló OP en su pregunta (aunque la diferencia en el tiempo no es tan grande, sino solo unos pocos segundos, dependiendo del tamaño de archivo).

Solución

La solución (si uno necesita cargar archivos de más de 1 MB y el tiempo de carga es importante para ellos) sería acceder al requestcuerpo como una secuencia. Según la documentación de Starlette , si accede .stream(), los fragmentos de bytes se proporcionan sin almacenar el cuerpo completo en la memoria (y luego en el directorio temporal, si el cuerpo contiene datos de archivos que superan 1 MB). A continuación se proporciona un ejemplo, donde el tiempo de carga se registra en el lado del cliente y que termina siendo el mismo que cuando se usa el marco Flask con el ejemplo dado en la pregunta de OP.

aplicación.py

from fastapi import Request
import aiofiles

@app.post('/upload')
async def upload(request: Request):
    try:
        filename = request.headers['filename']
        async with aiofiles.open(filename, 'wb') as f:
            async for chunk in request.stream():
                await f.write(chunk)
    except Exception:
        return {"message": "There was an error uploading the file"}
     
    return {"message": f"Successfuly uploaded {filename}"}

En caso de que su aplicación no requiera guardar el archivo en el disco y todo lo que necesite es cargar el archivo directamente en la memoria, puede usar lo siguiente (asegúrese de que su RAM tenga suficiente espacio disponible para acomodar los datos acumulados):

from fastapi import Request

@app.post('/upload')
async def upload(request: Request):
    body = b''
    try:
        filename = request.headers['filename']
        async for chunk in request.stream():
            body += chunk
    except Exception:
        return {"message": "There was an error uploading the file"}
    
    #print(body.decode())
    return {"message": f"Successfuly uploaded {filename}"}

prueba.py

import requests
import time

with open("images/1.png", "rb") as f:
    data = f.read()
   
url = 'http://127.0.0.1:8000/upload'
headers = {'filename': '1.png'}

start = time.time()
r = requests.post(url=url, data=data, headers=headers)
end = time.time() - start

print(f'Elapsed time is {end} seconds.', '\n')
print(r.json())

En caso de que tuviera que cargar un archivo bastante grande que no cabe en la RAM de su cliente (si, por ejemplo, tenía 2 GB de RAM disponibles en el dispositivo del cliente e intentó cargar un archivo de 4 GB), debería usar un Carga de streaming también en el lado del cliente, lo que le permitiría enviar secuencias o archivos grandes sin leerlos en la memoria (aunque la carga podría tomar un poco más de tiempo, dependiendo del tamaño del fragmento, que puede personalizar leyendo el archivo en fragmentos). en su lugar y establecer el tamaño del fragmento como se desee). Se dan ejemplos tanto en Python requestscomo httpx(que podrían producir un mejor rendimiento que requests).

prueba.py (usando requests)

import requests
import time

url = 'http://127.0.0.1:8000/upload'
headers = {'filename': '1.png'}

start = time.time()

with open("images/1.png", "rb") as f:
    r = requests.post(url=url, data=f, headers=headers)
   
end = time.time() - start

print(f'Elapsed time is {end} seconds.', '\n')
print(r.json())

prueba.py (usando httpx)

import httpx
import time

url = 'http://127.0.0.1:8000/upload'
headers = {'filename': '1.png'}

start = time.time()

with open("images/1.png", "rb") as f:
    r = httpx.post(url=url, data=f, headers=headers)
   
end = time.time() - start

print(f'Elapsed time is {end} seconds.', '\n')
print(r.json())

Para obtener más detalles y ejemplos de código (sobre cómo cargar varios archivos y datos de formulario/JSON) según el enfoque anterior (es decir, usar request.stream()el método), consulte esta respuesta .

Chris avatar Jan 11 '2022 13:01 Chris