与 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 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
它们。示例如下。有关def
vs的更多详细信息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
属性设置为1MB(1024 * 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 数据) ,请查看此答案。