子进程必须独立建连,不能复用父进程的gridfs实例;常见错误是直接传递不可pickle的fs对象导致卡住或brokenpipeerror,正确做法是每个子进程内新建mongoclient和gridfs,并在finally中关闭连接。

子进程必须独立建连,不能复用父进程的 GridFS 实例
常见错误是把 fs 对象直接传给 multiprocessing.Pool.map(),结果所有子进程卡住或抛 BrokenPipeError。根本原因:MongoDB 连接对象(MongoClient、GridFS)不可 pickle,也不能跨进程共享状态。
实操建议:
- 每个子进程函数内部调用
MongoClient()和gridfs.GridFS()(或GridFSBucket()),显式指定数据库名 - 避免在模块顶层或全局作用域初始化连接——否则 fork 时可能继承损坏的 socket
- 务必在
finally块中调用client.close(),防止连接数耗尽 - 连接串里必须带认证信息和目标库名,例如
"mongodb://user:pass@localhost:27017/mydb",不能只写"mongodb://localhost:27017"
用 concurrent.futures.ProcessPoolExecutor 替代原生 multiprocessing.Pool
ProcessPoolExecutor 更适合“导出一批文件”这类无状态任务:异常会原样抛出、结果自动收集、无需手动管理进程生命周期。
实操建议:
- 设
max_workers=4到8即可,再多反而压垮 MongoDB 连接池或网络带宽 - 别一次性
submit百万个任务——改用executor.map(download_one, file_id_list),它内部会分批调度 - 如果文件 ID 列表超大(如百万级),先用
itertools.islice分块读取,防内存堆积 - 加
timeout参数(如timeout=300),避免单个卡死拖垮整批
下载时必须流式读取,禁用 .read() 全量加载
一个 200MB 的视频,调一次 grid_out.read() 就会在该子进程中分配同等大小内存;10 个并发就吃掉 2GB+,系统 OOM Killer 很可能直接杀掉进程。
实操建议:
- 用
iter(lambda: grid_out.read(65536), b"")或手动while True+.read(65536)分块读,推荐 chunk_size = 64KB~1MB - 写入本地文件时用二进制模式:
open(path, "wb"),别用文本模式或io.BytesIO中转 - 文件名含斜杠、冒号、星号等非法字符时,必须清洗:
re.sub(r'[:"/\|?*]', "_", filename) - 不要依赖
file_obj.filename直接构造路径——同名文件多次上传会导致覆盖,应拼上str(file_obj._id)或uploadDate时间戳
如何安全处理百万级文件 ID 列表
直接 list(fs.find({})) 拿全部文档会把元数据全加载进内存,百万条记录轻松占掉几 GB。更糟的是,游标可能超时失效。
实操建议:
- 用
fs.find({}).batch_size(1000)控制每次从服务器拉多少条元数据 - 边遍历边提交任务,不用一次性存所有 ID:
for doc in fs.find({}): executor.submit(download_one, doc["_id"], doc["filename"]) - 对大集合加索引:
db.fs.files.createIndex({"filename": 1, "uploadDate": -1}),加速按名/时间筛选 - 如果只需导出最近 N 天的文件,用
fs.find({"uploadDate": {"$gt": dt}})过滤,别全量扫
.read(),或者忘了关 client。百万文件导出的关键不在“多”,而在“稳”——连接不泄漏、内存不暴涨、文件名不崩盘。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











