必须为 fs.chunks 创建 { files_id: 1, n: 1 } 唯一复合索引,否则 GridFS 下载将触发全集合扫描;该索引可加速按文件ID和序号精确查询chunk,防止重复写入,并需与分片策略(如 hashed files_id)对齐以避免跨分片广播。

fs.chunks 缺 { files_id: 1, n: 1 } 复合索引直接触发全集合扫描
GridFS 下载必须按 files_id 和序号 n 精确拉取每个 chunk,但默认只建了 _id 索引。没有 { files_id: 1, n: 1 },MongoDB 就只能扫完整个 fs.chunks 集合——一个 10MB 文件会生成 ~40 个 chunk,10 万文件就是 400 万 chunk 文档,每次下载都扫几百万行。
验证方式:db.fs.chunks.find({ files_id: ObjectId("...") }).explain("executionStats"),重点看 executionStats.totalDocsExamined 是否远大于 nReturned(比如返回 1 行却扫了 380 万)。
- 必须执行:
db.fs.chunks.createIndex({ files_id: 1, n: 1 }, { unique: true })——unique: true不仅加速查询,还能防重复写入导致文件损坏 - 别只建
{ files_id: 1 }单字段索引:它对按n排序读 chunk 完全无效,驱动仍会 fallback 到内存排序或跳过优化 - 建完立刻用
db.fs.chunks.getIndexes()确认该索引存在且状态为"ready"
驱动层隐式发起多次无约束查询,放大扫描代价
表面调一次 openDownloadStreamByName(),底层实际分两步:先查 fs.files 拿 _id,再拿这个 _id 去 fs.chunks 查所有 { files_id: ..., n: { $gte: 0 } } 的 chunk。如果 fs.chunks 没索引,第二步就是 N 次全表扫描(N = chunk 数量),不是一次。
- Node.js 中用老式
new mongodb.GridFS(db)更危险:它串行发每个 chunk 查询,且不带n范围限制,等于每 chunk 都扫一遍fs.chunks - 即使用了新
GridFSBucket,若没显式设maxTimeMS,网络抖动时重试会反复触发全表扫描 - 监控建议:在慢日志里搜
fs\.chunks,如果大量出现"nReturned":1,"totalDocsExamined":\d{6,},基本可锁定是这问题
分片集群中 fs.chunks 分片键错配,让扫描跨分片广播
在分片集群上,fs.files 和 fs.chunks 必须共置(co-located):即同一文件的元数据和所有 chunk 必须落在同一个分片上。否则 mongos 无法路由到单一节点,只能广播查询或跨分片 JOIN。
- 正确做法:
fs.files按业务字段(如user_id)范围分片,fs.chunks必须用{ files_id: "hashed" }分片——这是唯一能保证共置的方案 - 错误配置示例:
fs.files用{ uploadDate: 1 },fs.chunks却用{ n: 1 },结果一次下载可能触发 20+ 次跨分片请求 - 快速诊断:
db.runCommand({ explain: "find", collection: "fs.chunks", query: { files_id: ObjectId("...") } }),看输出里nShards是否 > 1
chunkSize 过小 + 无索引,让小文件也变成扫描炸弹
默认 chunkSize 是 255KB,对小文件(比如几十 KB 的图标、PDF 封面)意味着单个文件就生成多个 chunk。这些 chunk 的 files_id 相同、n 连续,但若没 { files_id: 1, n: 1 } 索引,MongoDB 就没法用索引跳转到起始 n,只能从头扫到尾找第一个匹配项。
- 后果比大文件更隐蔽:大文件慢大家会盯,而几千个 50KB 小文件并发下载,瞬间把
fs.chunks扫成热点 - 如果你的业务以小文件为主(如用户头像、文档封面),
chunkSize可适当调大到 512KB 或 1MB,减少 chunk 总数,但前提是已建好复合索引——否则只是把“扫 4 次”变成“扫 2 次” - 绝对不要靠增大
chunkSize来掩盖索引缺失:哪怕设成 8MB,只要没索引,第一次查询仍全表扫描
{ files_id: 1, n: 1 } 必须存在、必须生效、必须和分片策略对齐——少一个条件,查询就会退化成对 fs.chunks 的暴力穷举。



















