慢查询根因在索引缺失或 chunkSize 不适配:fs.files 需 filename/uploadDate 索引,fs.chunks 必须建 {files_id:1,n:1} 唯一复合索引;机械盘应调大 chunkSize 至 4MB 避免寻道爆炸。

查 slow query 日志,先定位是 files 还是 chunks 慢
直接看 mongod 日志里反复出现的 slow query 记录,重点抓 fs.files 和 fs.chunks 两个集合的耗时。如果慢查询集中在 fs.files,说明元数据查得慢;如果大量出现在 fs.chunks,基本就是 chunk 扫描没走索引。
快速验证方式:db.fs.files.find({ filename: "x.pdf" }).explain("executionStats")
看 executionStats.totalDocsExamined 是否远大于 nReturned(比如返回 1 条却扫了 8 万文档)——这是索引缺失的铁证。
- 若
totalDocsExamined ≈ nReturned,说明索引已命中,问题不在 files 查询 - 若
fs.chunks查询也慢,立刻运行db.fs.chunks.getIndexes(),确认是否存在{ files_id: 1, n: 1 }复合索引 - 别只建
{ files_id: 1 }单字段索引,它对按序读 chunk 完全无效
检查索引是否建对,尤其 metadata 字段别乱用 $in/$regex
业务里高频查 filename 或 uploadDate,但 fs.files 默认只有 _id 索引。不建对应索引,就是全表扫描。
正确做法:db.fs.files.createIndex({ filename: 1, uploadDate: -1 })
- 如果按项目 ID 查,别嵌在
metadata.projectId里用{ "metadata.projectId": "abc" }—— 提到顶层字段再建索引 - 避免
{ "metadata.tags": { $in: [...] } }这类查询,几乎无法走索引,考虑物化字段或改用普通集合 + 对象存储 -
db.fs.chunks.createIndex({ files_id: 1, n: 1 }, { unique: true })必须加unique: true,防重复 chunk 导致文件损坏
下载接口是否解析 Range 头并传给 openDownloadStream
Node.js 驱动里直接调 bucket.openDownloadStream() 默认读整个文件。客户端只请求视频某一段,服务端却拉全部 chunk,I/O 和内存压力翻倍。
必须从 HTTP Range header 解析出 start/end,显式传入:bucket.openDownloadStream(fileId, { start: 1024, end: 2048 })
- 大文件(>50MB)务必设超时:
stream.setTimeout(30000),防止慢连接占死连接池 - 别在单个请求里并发调多次
openDownloadStream()拼分片 —— GridFS 不是为高并发小块读设计的 - 优先用
bucket.openDownloadStreamByName("xxx")(MongoDB 4.4+),省掉一次fs.files查询
chunkSizeBytes 是否匹配硬件,机械盘上 255KB 是反模式
机械硬盘上 GridFS 慢,本质是 255KB 分块导致大量随机寻道。10MB 文件触发 40+ 次磁头移动,I/O 时间吃掉 80% 以上。
实测较优值:new GridFSBucket(db, { chunkSizeBytes: 4 * 1024 * 1024 })
- 已有数据无法在线修改
chunkSizeBytes:改files.chunkSize字段无效,必须重传 - SSD 上可放宽,但机械盘上必须调大;太小(256KB)→ 寻道爆炸,太大(16MB)→ 写入可能超 WiredTiger 16MB 文档上限
- WiredTiger 缓存对 GridFS 流式读几乎无作用,真正该盯的是 OS page cache 和
iostat -x 1的await值
真实瓶颈往往藏在索引和 chunkSize 的组合上:缺一个,另一个调再好也白搭。机械盘场景下,await > 20ms 且 %util ≈ 100% 时,别急着加副本集,先换 chunkSize、补索引、关重试。



















