GridFS适合传大文件,但需手动实现可控流式上传;PyMongo的upload_from_stream默认内存缓冲,须用BufferedIOBase包装文件句柄并设合适chunk_size,否则易OOM或残留碎片。

GridFS 适合传大文件吗?先看本质限制
GridFS 本身不是为“流式上传”设计的,它把文件切块(默认 255KB/chunk)存进 fs.chunks 集合,元信息存在 fs.files。真正决定能否“流式”的,是 PyMongo 的 GridFSBucket 是否支持边读边写——它支持,但必须手动控制 chunk 写入节奏,不能直接 upload_from_stream(file_obj) 就完事。
常见错误现象:
- 用
upload_from_stream()传 GB 级文件,内存暴涨甚至 OOM - 文件中途断开,
fs.files留下 incomplete 记录,fs.chunks存了一半碎片 - 没设
chunk_size_bytes,小文件变大量 tiny chunk,查询变慢
关键点:PyMongo 的 upload_from_stream() 内部确实流式处理,但它会把整个流读进内存缓冲区再分 chunk 发送——除非你给它一个真正按需吐数据的类文件对象。
用 io.BufferedIOBase 包装真实文件句柄
真正可控的流式上传,得自己实现一个可迭代、可 seek、且每次 read(n) 只返回 n 字节的类文件对象。别用 open() 返回的 TextIOWrapper(它是文本模式),也别用未缓冲的 io.RawIOBase 子类。
立即学习“Python免费学习笔记(深入)”;
使用场景:上传视频、日志归档包、数据库 dump 文件等 >100MB 的二进制内容。
import io
import os
from pymongo import MongoClient
from gridfs import GridFSBucket
<p>class FileStream(io.BufferedIOBase):
def <strong>init</strong>(self, path):
self.path = path
self._size = os.stat(path).st_size
self._offset = 0
self._file = open(path, "rb")</p><pre class='brush:php;toolbar:false;'>def readable(self):
return True
def read(self, size=-1):
if size == -1:
size = self._size - self._offset
data = self._file.read(min(size, self._size - self._offset))
self._offset += len(data)
return data
def seekable(self):
return True
def seek(self, offset, whence=0):
if whence == 0:
self._offset = max(0, min(offset, self._size))
elif whence == 1:
self._offset = max(0, min(self._offset + offset, self._size))
else:
self._offset = max(0, min(self._size + offset, self._size))
self._file.seek(self._offset)
return self._offset
def close(self):
self._file.close()注意:seek() 必须实现,否则 GridFSBucket 在校验或重试时会报 UnsupportedOperation: seek。
调用 upload_from_stream() 时的关键参数
upload_from_stream() 看似简单,但三个参数直接影响流控效果:
-
source:必须是你包装好的FileStream实例,不是路径字符串 -
chunk_size_bytes:建议设为1024 * 1024(1MB),太大单次 write 耗时长、失败重传成本高;太小则 chunk 数爆炸,fs.chunks索引膨胀 -
metadata:可加{"uploaded_at": datetime.utcnow(), "original_name": "xxx"},但别塞大字典,它存在fs.files,影响该集合查询性能
示例调用:
client = MongoClient("mongodb://localhost:27017")
db = client["myapp"]
fs = GridFSBucket(db, bucket_name="uploads")
<p>with FileStream("/tmp/large-video.mp4") as stream:
file_id = fs.upload_from_stream(
filename="large-video.mp4",
source=stream,
chunk_size_bytes=1024 * 1024,
metadata={"content_type": "video/mp4"}
)</p>不推荐用 upload_from_stream() 的 callback 参数做进度通知——它只在每个 chunk 写完后触发,无法反映网络延迟或 MongoDB 写入排队情况。
断点续传和清理残留要自己兜底
GridFS 没有原生断点续传。如果上传中途崩溃:
-
fs.files中该文件记录的length字段仍为 0 或不完整值 -
fs.chunks里已写入的 chunk 不会被自动清理
容易被忽略的地方:
- 每次上传前,先查
fs.files.find_one({"filename": "xxx"}),若存在且"length"与目标文件大小不一致,说明上次失败,应手动删掉fs.delete(file_id) - 不要用
fs.find({"filename": "xxx"}).count()判重——v4.4+ 已弃用 count(),改用fs.find({"filename": "xxx"}).limit(1).count_with_limit()或直接next(fs.find(...), None) -
fs.delete()是原子操作,但删除大文件时会阻塞其他 chunk 写入,线上环境建议在低峰期执行
流式上传这件事,难点不在代码几行,而在对 GridFS chunk 模型、PyMongo 缓冲策略、以及 MongoDB 写入确认机制的理解是否到位。随便套个 open() 就传,十次有八次会在 2GB 边界上出问题。


















