GridFS下载吞吐量低的首要原因是fs.chunks缺{files_id: 1}索引导致全表扫描,须手动创建该索引;其次需硬限并发流、对齐分片键、调大chunkSize并显式管控流生命周期。

GridFS 下载吞吐量卡在每秒几十 MB 甚至更低,不是网络或磁盘瓶颈,而是 fs.chunks 缺 { files_id: 1 } 索引导致全表扫描——这是高并发下载时最常被忽略、影响最大的单点。
为什么 fs.chunks 必须手动建 { files_id: 1 } 索引
驱动每次下载都要按 files_id 查所有 chunk 文档,没索引就是全表扫描。哪怕一个 2MB 文件只切 8 块,查询也可能扫几万行(尤其旧数据多时)。db.fs.chunks.getIndexes() 输出里没有 { "files_id": 1 },就必须立刻补:
db.fs.chunks.createIndex({ "files_id": 1 })- 别等“驱动自动建”——手动初始化桶、迁移历史数据、或用非默认桶名时,这个索引大概率缺失
- 加完后用
db.currentOp({ "ns": /^your_db_name\.fs\.chunks$/ })观察secs_running是否明显下降
并发流数量必须硬限流,不能靠连接池自动扛
每个 openDownloadStream() 占一个数据库连接 + 独立内存缓冲区。高并发下不是 GridFS 慢,而是 maxPoolSize 耗尽、Buffer 积压、或 OS 文件描述符打满。
- 用
p-limit(Node.js)或类似工具硬控并发数,建议 ≤ 5;前端做 Range 分片请求时更要严控 - 必须监听
error并显式调stream.destroy(),否则错误流残留会持续占连接和内存 - 响应层防超时:Nginx 默认
proxy_read_timeout 60s,大文件要调高,或定期res.write('')发心跳 - 用户断连时,Node.js 不会自动 cleanup,得靠
req.on('close', () => stream.destroy())
chunkSizeBytes 改大到 1MB~4MB 才能降 CPU 和解析开销
CPU 高的主因不是解压,是高频 BSON chunk 解析 + 内存拷贝。默认 255KB 分块让一个 50MB 文件要解析 200+ 次 BSON 文档,驱动反复反序列化 _id、n、data 字段。
- 创建桶时设
chunkSizeBytes: 4 * 1024 * 1024,chunk 数量直降 16 倍,BSON 解析次数同步减少 - 避免中间 Buffer 拷贝:用
openDownloadStreamByName()返回的Readable流,直接pipe()到响应或文件写入流 - 别用
decompressBufferSize——它只管 wire protocol 层解压,跟 GridFS 文件内容无关,调大反而可能触发 GC - 验证是否生效:在流上计数
data事件,平均文件解析 chunk 数 > 100 就说明 chunkSize 还太小
分片集群上 fs.files 和 fs.chunks 分片键必须对齐
如果两个集合分片键不一致,mongos 就得广播查询或跨分片 JOIN,一次下载可能触发几十次远程请求,延迟陡增。
-
fs.files推荐用业务字段如filename或user_id分片(避免时间戳类单调值) -
fs.chunks必须用files_id哈希分片:sh.shardCollection("mydb.fs.chunks", { "files_id": "hashed" }),保证 chunk 与元数据共置 - 分片前先确认
fs.chunks有{ files_id: 1 }索引,否则分片命令会失败 - 用
explain("executionStats")查fs.chunks.find({files_id: ObjectId()}),确认nShards等于实际分片数,且无scatter-gather模式
真正卡住吞吐的是索引缺失、流失控、分块太碎这三件事,而不是 MongoDB 本身或网络带宽。其中 { files_id: 1 } 索引最容易漏掉,也最立竿见影——上线前务必查一遍,别等压测崩了才想起来。

















