gridfs写丢失是因uploadfromstream分块写入(fs.files+fs.chunks)非原子、默认不可重试且无事务保障三者叠加所致;retrywrites=true可能失效于驱动版本低、非副本集拓扑或fcv不合规。

GridFS 写丢失不是切换本身导致的,而是底层分块写入 + 默认不可重试 + 无事务保障三者叠加的结果
uploadFromStream 实际是多次独立写入,不是原子操作
GridFS 的 uploadFromStream 看似一个 API 调用,背后却执行了两组非原子操作:
- 先向
fs.files插入一条元数据文档(含_id、length、chunkSize等) - 再将文件按默认
chunkSizeBytes: 255 * 1024拆成 N 个 chunk,逐个调用insertOne写入fs.chunks
这 N+1 次写入彼此隔离。主节点在写第 127 个 chunk 时发生故障转移,fs.files 已落库,但后续 chunk 全部丢失——最终出现“文件存在但下载报 Corrupted download: chunk size mismatch”。
retryWrites=true 对 GridFS 失效的常见原因
即使连接串加了 retryWrites=true,GridFS 仍可能静默跳过重试,因为:
- 驱动版本低于要求:Node.js 驱动需 ≥ 3.0,PyMongo ≥ 3.6,Java ≥ 3.6
- 集群拓扑不合规:单机
mongod或未启用副本集(rs.initiate()未执行) - 服务端
featureCompatibilityVersiondb.adminCommand({getCmdLineOpts: 1}) 查) - 上传被包在自定义
ClientSession中,且 session 未启用重试(session.startTransaction({retryWrites: true})不等于全局重试)
writeConcern 不显式设置,多数确认就形同虚设
GridFS bucket 构造时不传 write_concern 参数,所有写入都走默认 w: 1。这意味着:
-
fs.files插入成功只等主节点返回,不等从节点复制 - 每个
fs.chunks文档也只等主节点确认,哪怕members配置了priority: 1 - 主节点宕机前未同步的 chunk 永久丢失,而元数据仍在,形成“半截文件”
正确做法是:bucket = GridFSBucket(db, write_concern=WriteConcern(w="majority", j=True))——注意 j: True 是防掉电丢 chunk,不是解决复制延迟。
真正兜底的方案必须组合使用
靠单一配置无法根治写丢失,必须同时做三件事:
- 启用
retryWrites=true并验证生效(用rs.stepDown()+ 小文件上传测试) - bucket 实例化时硬编码
write_concern=WriteConcern(w="majority", j=True) - 业务层实现失败清理:上传前生成唯一
uploadId存入metadata,失败后用db.fs.chunks.deleteMany({"metadata.uploadId": "xxx"})清脏数据,再删fs.files文档
最容易被忽略的是第三点:GridFS 没有回滚机制,NotPrimaryError 后若不主动清理,孤儿 chunk 会持续占用空间并干扰后续查询。











