根本瓶颈是机械硬盘寻道延迟,因默认255KB chunk导致10MB文件切分40+次随机IO;需调大chunkSizeBytes至4MB、复用bucket实例、补建{files_id:1,n:1}复合索引,并确认副本集节点挂载独立物理盘。

GridFS读取并发卡在机械硬盘寻道上
根本瓶颈不是MongoDB配置或网络,而是机械硬盘物理特性:默认chunkSizeBytes为255KB,导致10MB文件被切成40+个chunk,每个都可能落在磁盘不同扇区。并发一上来,磁头疯狂寻道,iostat -x 1里await飙到50ms以上、%util长期100%,就是它在喊救命。
- 别信“加节点就能分流”——副本集从节点若挂载同一块机械盘,实际仍是串行寻道,还多一次网络转发
- 先用
lsblk和cat /proc/mounts确认secondary节点的/var/lib/mongodb是否真指向独立物理盘(不是LVM或软链接) - SSD上不明显,但只要还在用机械盘,调
readPreference或maxPoolSize都是白忙
fs.chunks缺少{ files_id: 1, n: 1 }复合索引
GridFS读取分两步:先查fs.files拿元数据,再按files_id和序号n去fs.chunks拼数据。第二步若无该索引,就会全表扫描——而chunk数量通常是文件数的几百倍,查询延迟直接雪崩。
- 立刻执行:
db.fs.chunks.createIndex({ files_id: 1, n: 1 }, { unique: true }) - 别建单字段
{ files_id: 1 }索引,它无法加速按n顺序读取chunk的过程 - 验证是否生效:
db.fs.chunks.find({ files_id: ObjectId("...") }).explain("executionStats"),看totalDocsExamined是否接近nReturned
每次读都新建GridFSBucket实例
新建GridFSBucket会清空内部缓冲、重建连接池、重新解析配置,机械硬盘下初始化延迟肉眼可见。高并发时,这一步就吃掉大量时间,还放大随机IO压力。
- 必须全局复用同一个
bucket实例,而不是每次new GridFSBucket(db, {...}) - 尤其避免在HTTP handler里现场创建——Node.js或PyMongo里都应作为单例注入
- 如果用
openDownloadStreamByName,确保传入的是复用后的bucket,而非每次都new
WiredTiger缓存对GridFS读几乎没用
调大cacheSizeGB不会提升GridFS读取吞吐,因为WiredTiger只缓存B-tree页面(索引、小文档),而fs.chunks的BinData是逐个查询+解包的直通路径,完全绕过page cache。监控db.runCommand({serverStatus: 1}).wiredTiger.cache时,bytes currently in the cache基本不涨。
- 真正该盯的是OS page cache效率:
4MB chunkSize让单次读更接近顺序IO,提升page cache复用率;255KB则反复换页,unmodified pages evicted指标会异常高 - 已有数据无法在线改
chunkSizeBytes——改files.chunkSize字段无效,驱动和服务器都不识别,必须重传 - 新建bucket时必须显式传参:
new GridFSBucket(db, { chunkSizeBytes: 4 * 1024 * 1024 })
真正卡住并发的,往往不是代码写得不够“并发”,而是底层IO路径里某一个环节在机械盘上彻底堵死——比如chunkSize没调、索引没建、实例没复用,三者叠加,再多线程也只在排队等磁盘响应。


















