做后端这行,迟早会撞上一道绕不开的坎------用户要传一个几个 G 甚至几十个 G 的文件,网络还时断时续。这时候如果还用最朴素的 UploadFile 一把梭,内存爆掉是轻的,用户传到 99% 网络一抖直接从头再来才是真的崩溃。这篇文章就把 FastAPI 场景下处理大文件的几条主流路线捋一遍,顺带给出一套能直接抄作业的断点续传实现。
大文件传输的几种技术路线
面对大文件,业界基本上分化出了三条路子,各有各的适用场景。
第一条路 是让 FastAPI 亲自上阵,用流式读写扛住大文件,服务端一边接收一边写盘,全程不把整个文件塞进内存。这条路简单直接,适合中等规模、自建存储的场景,用 异步文件 IO 配合合理的分块大小(业内建议 1MB 到 4MB 之间)就能跑得很稳。不过要命的地方在于,如果文件真的很大(比如 3GB 往上),普通的 python-multipart 解析方式和客户端库都得换成支持流式传输的版本,否则内存和超时问题照样找上门。
第二条路 是把 FastAPI 从传输链路里请出去,只负责发一张预签名 URL(presigned URL),真正的大文件数据直接从客户端怼到对象存储(S3、MinIO 之类)上。有人专门做过基准测试,结论挺扎心------对于真正的大文件,最快的方式就是干脆绕过 FastAPI,让客户端直连对象存储。这也是目前云原生场景下的主流做法,服务端压力几乎为零,代码里能看到完整的实现范例,用 FastAPI 生成签名、MongoDB 记录上传状态、S3 承载实际数据。
第三条路 是搞一套标准化的断点续传协议,代表就是 tus 协议。它是一个基于 HTTP 的开放协议,专门为可恢复上传设计,客户端和服务端都有现成实现,社区里已经有人把它移植到 FastAPI 上了。如果你的产品需要跨平台、跨客户端统一的续传体验(比如既有 Web 又有移动端),走 tus 这条路能省很多重复造轮子的功夫。
三条路线放在一张图里看会更清楚:

存储后端怎么选
存储后端这一层,决定了后面断点续传怎么落地、扩容怎么扩。几个常见选项摆在一起对比一下会更直观。
| 存储方案 | 实现复杂度 | 扩展性 | 断点续传支持 | 适用场景 |
|---|---|---|---|---|
| 本地磁盘 | 低,直接写文件系统 | 差,受单机容量限制 | 需要自己实现分片+合并 | 小规模、内网工具、原型验证 |
| NFS/分布式文件系统 | 中,需运维挂载 | 中等 | 同本地磁盘,逻辑复用 | 多机共享但预算有限的团队 |
| S3 / MinIO(对象存储) | 中,需要 SDK 接入 | 好,天然分布式 | 原生支持 Multipart Upload | 云原生、大规模、生产环境首选 |
| tus 服务端(可搭配任意后端) | 中偏高,需部署协议层 | 取决于底层存储 | 协议层原生保证 | 需要标准化跨端续传能力的产品 |
对象存储这一档基本是当前生产环境的最优解,因为 S3 的 Multipart Upload 机制本身就是为大文件而生的------先发起一个上传任务拿到 upload_id,然后把文件切成多个分片并发上传,每片单独返回 ETag,最后调用一次合并接口把所有分片拼成完整对象。这套机制天然就带着断点续传的血统,因为哪个分片传失败了,重传那一片就行,完全不用推倒重来。
断点续传的核心原理
断点续传说白了就是解决两个问题------上传中断了怎么知道传到哪了 ,以及下载中断了怎么从断点继续拉。这两件事的技术手段完全不同,值得分开看。
上传方向:分片 + 状态记录
上传的断点续传,核心思路是把大文件切成固定大小的小块,每块独立上传,服务端记录每块的上传状态。假设文件总大小为 S,每片大小为 C,那么总分片数就是
N=⌈CS⌉
客户端在上传前先算好这个 N,然后带着文件的唯一标识(通常用文件内容的哈希值,比如 SHA-256)去问服务端这个文件传到哪一片了,服务端一查记录,把已完成的分片索引列表返回,客户端就从下一片开始接着传,这就是断点续传最朴素也最通用的实现方式。
下载方向:HTTP Range 请求
下载的续传靠的是 HTTP 协议自带的 Range 头。客户端在请求头里写上 Range: bytes=1000000-,服务端识别到这个头以后,不再返回完整文件,而是从指定字节位置开始读取,返回状态码 206(Partial Content),配合 Content-Range 响应头告诉客户端这次返回的是文件的哪一段。下载工具(比如浏览器、迅雷、断点续传的脚本)本身就是靠这套机制实现暂停后继续下载的。
整个断点续传的时序,可以用一张图串起来:

完整案例:FastAPI + MinIO 实现分片上传/下载与断点续传
下面这套方案拿的是 MinIO(S3 协议兼容的对象存储,本地部署也很方便)当存储后端,配合 SQLite 记录分片状态,逻辑可以直接迁移到生产环境的 PostgreSQL 和真正的 S3 上。整体项目结构是这样的。
bash
big_file_service/
├── main.py # FastAPI入口
├── storage.py # 存储后端封装(MinIO/S3)
├── models.py # 分片状态数据模型
├── upload.py # 上传相关接口
├── download.py # 下载相关接口(支持Range)
└── requirements.txt
1. 存储后端封装
用一层抽象把具体的存储实现盖住,以后想换存储后端(比如从 MinIO 换成阿里云 OSS)只需要改这一个文件。
python
# storage.py
from minio import Minio
from minio.error import S3Error
import os
class ObjectStorage:
def __init__(self):
self.client = Minio(
"localhost:9000",
access_key="minioadmin",
secret_key="minioadmin",
secure=False
)
self.bucket = "big-files"
if not self.client.bucket_exists(self.bucket):
self.client.make_bucket(self.bucket)
def save_chunk(self, upload_id: str, chunk_index: int, data: bytes):
"""保存单个分片"""
object_name = f"chunks/{upload_id}/{chunk_index}"
from io import BytesIO
self.client.put_object(
self.bucket, object_name, BytesIO(data), length=len(data)
)
def merge_chunks(self, upload_id: str, total_chunks: int, final_name: str):
"""合并所有分片为完整文件, 使用S3的compose_object能力"""
from minio.commonconfig import ComposeSource
sources = [
ComposeSource(self.bucket, f"chunks/{upload_id}/{i}")
for i in range(total_chunks)
]
self.client.compose_object(self.bucket, final_name, sources)
# 清理临时分片
for i in range(total_chunks):
self.client.remove_object(self.bucket, f"chunks/{upload_id}/{i}")
def get_object_stream(self, object_name: str, offset: int = 0, length: int = None):
return self.client.get_object(
self.bucket, object_name, offset=offset, length=length
)
def stat_object(self, object_name: str):
return self.client.stat_object(self.bucket, object_name)
storage = ObjectStorage()
这里用到的 compose_object 就是对象存储版的 Multipart Upload 合并逻辑,不用自己在服务器本地拼文件,直接在存储层完成,效率高很多。
2. 分片状态记录
用一张简单的表记录每个上传任务的进度,生产环境建议换 Redis 存这类高频读写的状态数据。
python
# models.py
import sqlite3
conn = sqlite3.connect("upload_state.db", check_same_thread=False)
conn.execute("""
CREATE TABLE IF NOT EXISTS upload_sessions (
upload_id TEXT PRIMARY KEY,
filename TEXT,
file_hash TEXT,
total_size INTEGER,
chunk_size INTEGER,
total_chunks INTEGER,
uploaded_chunks TEXT DEFAULT '',
status TEXT DEFAULT 'uploading'
)
""")
conn.commit()
def create_session(upload_id, filename, file_hash, total_size, chunk_size, total_chunks):
conn.execute(
"INSERT OR IGNORE INTO upload_sessions VALUES (?,?,?,?,?,?,?,?)",
(upload_id, filename, file_hash, total_size, chunk_size, total_chunks, '', 'uploading')
)
conn.commit()
def get_session_by_hash(file_hash):
row = conn.execute(
"SELECT * FROM upload_sessions WHERE file_hash=?", (file_hash,)
).fetchone()
return row
def mark_chunk_done(upload_id, chunk_index):
row = conn.execute(
"SELECT uploaded_chunks FROM upload_sessions WHERE upload_id=?", (upload_id,)
).fetchone()
done = set(row[0].split(',')) if row[0] else set()
done.add(str(chunk_index))
conn.execute(
"UPDATE upload_sessions SET uploaded_chunks=? WHERE upload_id=?",
(",".join(done), upload_id)
)
conn.commit()
def get_uploaded_chunks(upload_id):
row = conn.execute(
"SELECT uploaded_chunks FROM upload_sessions WHERE upload_id=?", (upload_id,)
).fetchone()
if not row or not row[0]:
return set()
return set(int(x) for x in row[0].split(','))
3. 上传接口------初始化、上传分片、查询进度、完成合并
这是整个方案的核心,四个接口配合起来就实现了完整的断点续传。
python
# upload.py
from fastapi import APIRouter, UploadFile, Form
from storage import storage
from models import create_session, get_session_by_hash, mark_chunk_done, get_uploaded_chunks
import uuid, math
router = APIRouter(prefix="/upload")
@router.post("/init")
async def init_upload(filename: str = Form(...), file_hash: str = Form(...),
total_size: int = Form(...), chunk_size: int = Form(4 * 1024 * 1024)):
"""初始化上传, 若文件hash已存在记录, 说明是续传, 直接返回已完成分片"""
existing = get_session_by_hash(file_hash)
if existing:
upload_id = existing[0]
uploaded = get_uploaded_chunks(upload_id)
return {"upload_id": upload_id, "uploaded_chunks": sorted(uploaded), "resumed": True}
upload_id = str(uuid.uuid4())
total_chunks = math.ceil(total_size / chunk_size)
create_session(upload_id, filename, file_hash, total_size, chunk_size, total_chunks)
return {"upload_id": upload_id, "uploaded_chunks": [], "resumed": False, "total_chunks": total_chunks}
@router.post("/chunk")
async def upload_chunk(upload_id: str = Form(...), chunk_index: int = Form(...),
file: UploadFile = None):
"""上传单个分片, 已上传过的分片直接跳过, 天然支持重复请求"""
uploaded = get_uploaded_chunks(upload_id)
if chunk_index in uploaded:
return {"status": "already_uploaded", "chunk_index": chunk_index}
data = await file.read()
storage.save_chunk(upload_id, chunk_index, data)
mark_chunk_done(upload_id, chunk_index)
return {"status": "ok", "chunk_index": chunk_index}
@router.post("/complete")
async def complete_upload(upload_id: str = Form(...), filename: str = Form(...),
total_chunks: int = Form(...)):
"""所有分片上传完成后触发合并"""
storage.merge_chunks(upload_id, total_chunks, filename)
return {"status": "completed", "filename": filename}
客户端这边的配合逻辑很简单------先算文件哈希(推荐用文件内容的 SHA-256,或者为了速度只对前几MB采样计算),拿哈希去调 /upload/init,服务端要是发现这个哈希以前传过一部分,直接把已完成分片列表甩回来,客户端跳过这些分片,接着传剩下的就行。网络断了、页面刷新了都不怕,下次拿同一个哈希重新 init 一次就能接着传,这就是最朴素但极其好用的断点续传逻辑。
4. 下载接口------支持 Range 请求实现断点续传
python
# download.py
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
from storage import storage
router = APIRouter(prefix="/download")
@router.get("/{filename}")
async def download_file(filename: str, request: Request):
stat = storage.stat_object(filename)
file_size = stat.size
range_header = request.headers.get("range")
if range_header:
# 解析 Range: bytes=1000000-
range_value = range_header.replace("bytes=", "")
start_str, end_str = range_value.split("-")
start = int(start_str)
end = int(end_str) if end_str else file_size - 1
length = end - start + 1
stream = storage.get_object_stream(filename, offset=start, length=length)
def iter_stream():
for chunk in stream.stream(32 * 1024):
yield chunk
stream.close()
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(length),
}
return StreamingResponse(iter_stream(), status_code=206, headers=headers)
# 没有Range头, 走完整下载
stream = storage.get_object_stream(filename)
def iter_full():
for chunk in stream.stream(32 * 1024):
yield chunk
stream.close()
headers = {"Content-Length": str(file_size), "Accept-Ranges": "bytes"}
return StreamingResponse(iter_full(), headers=headers)
这里的关键就是识别客户端带没带 Range 头,带了就切片读取并返回 206,没带就照常全量返回。像迅雷、浏览器自带下载器、wget -c、curl -C - 这些工具,全都是靠识别服务端返不返回 Accept-Ranges: bytes 来判断能不能续传的。
5. 挂载路由与部署要点
python
# main.py
from fastapi import FastAPI
from upload import router as upload_router
from download import router as download_router
app = FastAPI(title="Big File Service")
app.include_router(upload_router)
app.include_router(download_router)
真正上线前还有几个坑得提前填。
- 反向代理限制 ------如果前面挂了 Nginx,记得把
client_max_body_size调大,不然大文件请求直接被 Nginx 拦在门外,报 413 错误,跟 FastAPI 代码写得好不好完全没关系。 - 超时设置------分片上传单次请求通常很快,但完整文件的合并操作(尤其本地磁盘方案)可能耗时较长,网关和反代的超时时间要相应放宽。
- 客户端并发控制------多个分片可以并发上传提高速度,但不建议无限并发,一般 3 到 6 个并发连接是比较均衡的选择,能兼顾速度和服务端压力。
- 清理策略------上传中断后遗留的孤儿分片得有个定时任务清理,不然对象存储的存储空间会被这些垃圾数据慢慢吃掉。
- 如果不想自己实现分片协议------直接上 tus 服务端方案是个稳妥的选择,协议成熟、客户端库齐全(JS、iOS、Android都有),社区维护的 FastAPI 集成方案也已经跑通了实际案例。
几条路线该怎么选
把上面讲的东西收个尾,几种方案的取舍其实可以简化成一句话------文件不大、内网工具,用流式直传就够 ;文件很大、上云生产、追求省心,直接上预签名URL让客户端直连对象存储 ;产品形态复杂、多端统一、需要标准化断点续传体验,上 tus 。三条路没有绝对的优劣,关键看你的团队规模、运维能力和产品对续传体验的要求。自己写一套分片+状态记录的逻辑(就像上面的案例)好处是完全可控,坏处是维护成本要自己扛;用现成的协议或云服务好处是省心,坏处是多了一层依赖。这事儿说到底还是工程上永恒的那道选择题------自己造轮子 还是 站在巨人肩膀上。
参考资料
I benchmarked 5 different FastAPI file upload methods, Reddit r/Python, www.reddit.com/r/Python/co...
Streaming File Uploads and Downloads with FastAPI, Python in Plain English, python.plainenglish.io/streaming-f...
How to Upload a large File (≥3GB) to FastAPI backend?, Stack Overflow, stackoverflow.com/questions/7...
FastAPI File Uploads --- Clean, Fast, and Foolproof, Medium, medium.com/@ThinkingLo...
liviaerxin/fastapi-tusd, GitHub, github.com/liviaerxin/...
tus - resumable file uploads, tus.io, tus.io/
Why Chunked & Resumable Uploads Are a Game Changer for Video Processing, Medium, aditya007.medium.com/why-chunked...
Python server tus implementation with FastAPI, Transloadit Community, community.transloadit.com/t/python-se...
nicholasadamou/s3-large-file-uploader, GitHub, github.com/nicholasada...
Uploading large objects to Amazon S3 using multipart upload and transfer acceleration, AWS Blog, aws.amazon.com/blogs/compu...
How to Implement File Uploads in FastAPI, OneUptime Blog, oneuptime.com/blog/post/2...