必须为 fs.chunks 创建 { "files_id": 1 } 索引,否则并发下载会触发全表扫描导致卡死;Node.js 和 Java 必须使用 GridFSBucket 而非过时类;需硬限并发流数并显式销毁;分片集群中 fs.files 与 fs.chunks 分片键必须严格一致。

fs.chunks 缺 { files_id: 1 } 索引直接拖垮并发性能
没这个索引,每个 openDownloadStream() 都会触发对 fs.chunks 的全表扫描——哪怕只下 1MB 文件,也可能扫数万行。并发一上来,CPU 和磁盘 I/O 瞬间拉满,不是慢,是“卡死”。
- 执行
db.fs.chunks.getIndexes(),确认输出里有{ "files_id": 1 } - 没有就立刻建:
db.fs.chunks.createIndex({ "files_id": 1 }) - 别信“驱动会自动建”:手动创建集合、迁移旧数据、或用非默认桶名初始化时,索引大概率缺失
- 加完后用
db.currentOp({ "ns": /^your_db_name\.fs\.chunks$/ })观察查询的secs_running是否明显下降
Node.js 中必须用 GridFSBucket,禁用所有老式 GridFS 类
还在用 new mongodb.GridFS(db) 或 GridFSDBFile?那根本不是并发问题,是内存和连接池在爆炸。老类串行查 chunk、全量加载到内存、不支持背压,一个 100MB 文件就能让进程 OOM。
- Node.js 必须用
new mongodb.GridFSBucket(db),下载走openDownloadStream()或openDownloadStreamByName() - Java 项目必须迁移到
com.mongodb.client.gridfs.GridFSBucket,彻底弃用GridFSDBFile - 老类无法复用连接池,每个下载请求都新建 socket,高并发下
maxPoolSize迅速耗尽
并发流数量必须硬限,且每个流生命周期要显式管理
看似只是“开多个流读同一文件”,实际每个 openDownloadStream() 都占一个数据库连接 + 独立缓冲区 + 文件描述符。没限制时,50 个并发可能打满 MongoDB 连接池,或触发 Linux EMFILE 错误。
- 用
p-limit控制同时打开的流数,建议 ≤ 5(尤其前端做 Range 分片请求时) - 必须监听
error事件并调用stream.destroy(),否则失败流残留会持续占资源 - 响应层防超时:Nginx 默认
proxy_read_timeout 60s,大文件得调高,或定期res.write('')发心跳 - 用户关浏览器?
req.on('close', () => stream.destroy())是唯一可靠 cleanup 方式
分片集群上 fs.files 和 fs.chunks 分片键必须严格一致
如果两个集合分片策略不匹配,mongos 就得广播查询或跨分片 JOIN——一次下载可能触发几十次远程请求,延迟从几十毫秒跳到几秒。这不是代码问题,是架构配置错误。
- 检查
sh.status(),确认fs.files和fs.chunks使用完全相同的分片键(推荐_id) - 不要用
filename当分片键:同名文件多版本会导致路由混乱 - 已上线集群需重分片时,先停写、导出元数据、重建分片键、再导入,不能在线改

















