与 Flask 相比,FastAPI UploadFile 速度较慢

2024-12-27 08:46:00
admin
原创
133
摘要:问题描述:我已经创建了一个 FastAPI 端点,如下所示:@app.post("/report/upload") def create_upload_files(files: UploadFile = File(...)): try: with o...

问题描述:

我已经创建了一个 FastAPI 端点,如下所示:

@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__()}

它是通过 uvicorn 启动的:

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

我正在使用 Python执行一些上传约100 MB文件的测试requests,大约需要128 秒才能完成:

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)

我使用 Flask 的 API 端点测试了相同的上传脚本,大约需要0.5 秒

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)

我做错什么了吗?


解决方案 1:

您可以在使用 normal 定义端点后使用同步写入来写入文件def,如本答案所示,或者在用 定义端点后使用异步写入(使用aiofilesasync def )来写入文件—UploadFile方法就是async方法,因此您需要使用await它们。示例如下。有关defvs的更多详细信息async def,以及选择其中一个可能会如何影响您的 API 性能(取决于端点内执行的任务的性质),请查看此答案

上传单个文件

应用程序

from fastapi import File, UploadFile, HTTPException
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:
        raise HTTPException(status_code=500, detail='Something went wrong')
    finally:
        await file.close()

    return {"message": f"Successfuly uploaded {file.filename}"}
分块读取文件

正如此答案中所述,FastAPI/Starlette 在底层使用SpooledTemporaryFile,其max_size属性设置为 1 MB,这意味着数据在内存中缓冲,直到文件大小超过 1 MB,此时数据将写入磁盘上的临时文件,因此,调用await file.read()实际上会将数据从磁盘读入内存(如果上传的文件大于 1 MB)。因此,您可能希望async以分块方式使用,以避免将整个文件加载到内存中,这可能会导致问题 - 例如,如果您有 8GB 的​​ RAM,则无法加载 50GB 的文件(更不用说可用的 RAM 总是小于安装的总量,因为本机操作系统和计算机上运行的其他应用程序将使用部分 RAM)。因此,在这种情况下,您应该将文件分块加载到内存中,然后一次处理一个数据块。但是,此方法可能需要更长时间才能完成,具体取决于您选择的块大小;下面是1024 * 1024字节(= 1MB)。您可以根据需要调整块大小。

from fastapi import File, UploadFile, HTTPException
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:
        raise HTTPException(status_code=500, detail='Something went wrong')
    finally:
        await file.close()

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

或者,您可以使用shutil.copyfileobj(),它用于将一个file-like对象的内容复制到另一个file-like对象(也请参阅此答案1024 * 1024)。默认情况下,数据以块为单位读取,默认缓冲区(块)大小为 1MB(即字节)(对于 Windows)和 64KB(对于其他平台)(请参阅此处的源代码)。您可以通过传递可选参数来指定缓冲区大小length。注意:如果length传递了负值,则将读取文件的整个内容 -f.read()另请参阅文档,其中介绍了.copyfileobj()幕后用途。的源代码可以在这里.copyfileobj()找到- 在读取/写入文件内容方面,它与以前的方法并没有什么不同。但是,在后台使用阻塞 I/O 操作,这将导致阻塞整个服务器(如果在端点内使用)。因此,为了避免这种情况,您可以使用 Starlette在单独的线程中运行所有需要的函数(然后等待)以确保主线程(运行协程的位置)不会被阻塞。当你调用对象的方法时,FastAPI 内部会使用完全相同的函数,即 、、等——请参阅此处的源代码。示例:.copyfileobj()`async defrun_in_threadpool()asyncUploadFile.write().read()close()`

from fastapi import File, UploadFile, HTTPException
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:
        raise HTTPException(status_code=500, detail='Something went wrong')
    finally:
        if 'f' in locals(): await run_in_threadpool(f.close)
        await file.close()

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

测试.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())

有关 HTML<form>示例,请参见此处。

上传多个文件

应用程序

from fastapi import File, UploadFile, HTTPException
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:
            raise HTTPException(status_code=500, detail='Something went wrong')
        finally:
            await file.close()

    return {"message": f"Successfuly uploaded {[file.filename for file in files]}"}  
分块读取文件

要分块读取文件,请参阅本答案前面描述的方法。

测试.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())

有关 HTML<form>示例,请参见此处。

更新

深入研究源代码,似乎Starlette的最新版本(FastAPI 在下面使用)使用SpooledTemporaryFile(用于UploadFile数据结构)的max_size属性设置为1MB1024 * 1024字节) - 请参阅此处- 与旧版本相比,旧版本max_size将其设置为默认值,即 0 字节,例如此处的那个。

上面的意思是,在过去,无论文件大小如何,数据都会完全加载到内存中(如果文件无法放入 RAM,则可能导致问题),而在最新版本中,数据会在内存中缓冲,直到大小file超过max_size(即 1MB),此时内容会写入磁盘;更具体地说,写入操作系统的临时目录(注意:这也意味着您可以上传的文件的最大大小受限于系统临时目录可用的存储空间。如果您的系统有足够的存储空间(满足您的需要),则无需担心;否则,请查看有关如何更改默认临时目录的答案)。因此,多次写入文件的过程 - 即最初将数据加载到RAM中,然后,如果数据大小超过1MB,则将文件写入临时目录,然后从临时目录读取文件(使用file.read()),最后将文件写入永久目录 - 与使用Flask框架相比,上传文件的速度很慢,正如OP在他们的问题中指出的那样(虽然时间差异不是很大,但只有几秒钟,取决于文件的大小)。

解决方案

解决方案(如果需要上传大于 1MB 的文件,并且上传时间对他们来说很重要)是将主体request作为流进行访问。根据Starlette 文档,如果您访问.stream(),则将提供字节块,而无需将整个主体存储到内存中(如果主体包含超过 1MB 的文件数据,则稍后存储到临时目录中)。下面给出了一个示例,其中上传时间记录在客户端,最终与使用 Flask 框架时与 OP 问题中给出的示例相同。

应用程序

from fastapi import Request, HTTPException
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:
        raise HTTPException(status_code=500, detail='Something went wrong')
     
    return {"message": f"Successfuly uploaded {filename}"}

如果您的应用程序不需要将文件保存到磁盘,而您需要的只是将文件直接加载到内存中,那么您可以使用以下命令(确保您的 RAM 有足够的空间来容纳累积的数据):

from fastapi import Request, HTTPException

@app.post('/upload')
async def upload(request: Request):
    chunks = []
    try:
        filename = request.headers['filename']
        async for chunk in request.stream():
            chunks.append(chunk)
        body = b''.join(chunks)
    except Exception:
        raise HTTPException(status_code=500, detail='Something went wrong')
    
    return {"message": f"Successfuly uploaded {filename}"}

测试.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.', '
')
print(r.json())

如果您必须上传一个相当大的文件,而客户端的 RAM 无法容纳(例如,如果客户端设备上有 2 GB 的可用 RAM 并尝试加载 4 GB 的文件),您也应该在客户端使用流式上传,这样您就可以发送大型流或文件而无需将它们读入内存(但可能需要更多时间才能上传,具体取决于块大小,您可以通过分块读取文件并根据需要设置块大小来自定义块大小)。 Pythonrequests和中都给出了示例httpx(可能比 性能更好requests)。

test.py (使用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.', '
')
print(r.json())

test.py (使用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.', '
')
print(r.json())

有关基于上述方法(即使用方法)的更多详细信息代码示例request.stream()(关于上传多个文件和表单/JSON 数据) ,请查看此答案

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用