gridfs并发上传卡在20–30 qps,根本原因是fs.files集合的insertone()操作成为串行瓶颈;所有openuploadstream()均同步执行该操作,复用单bucket加剧竞争,而预分配id(openuploadstreamwithid)或按业务分桶可彻底隔离锁。

GridFS并发上传卡在20–30 QPS,不是MongoDB慢,而是默认行为把fs.files元数据写入变成了串行点——所有上传都排队等同一个insertOne完成。
为什么openUploadStream会排队等fs.files写入
GridFS本身不加锁,但底层fs.files集合的insertOne()操作在高并发下形成隐式竞争。每个openUploadStream()调用都会同步执行这一步,而MongoDB 4.0+虽支持集合级意向锁,可fs.files的首次插入仍需排他性协调。监控时能看到fs.files写延迟飙升,fs.chunks却空闲。
- 复用单个
GridFSBucket实例是常见诱因:所有goroutine/协程/线程共用同一bucket,天然串行 - 没指定
bucketName时,默认都打到fs这个通用bucket,锁域没隔离 - MongoDB 4.2起优化了
fs.chunks批量插入的write concern,但fs.files仍无此类优化
绕过fs.files锁竞争的两种实操方式
真正提升吞吐的关键不是调缓存,而是跳过元数据写入的同步等待。预分配ID或分桶是最直接有效的路径。
- 用
openUploadStreamWithId()+ 预生成ObjectId:上传前生成唯一ID,传入后GridFS跳过fs.files.insertOne(),直接写fs.chunks - 按业务维度分
bucketName:比如"uploads_user_123"、"uploads_batch_2026q3",不同bucket对应独立的fs.files和fs.chunks集合,锁完全隔离 - 别手动先
insertOne到fs.files:会破坏GridFS的chunk分片逻辑,后续openDownloadStream读取失败
chunkSizeBytes设多大才不影响并发写入
chunkSizeBytes不改变锁粒度,但直接影响每秒写入次数和journal压力。设得太小(如16KB),1GB文件要发4万次插入请求;太大(如4MB),单次写入耗时拉长,流响应变卡。
- 千兆内网+WiredTiger引擎下,
256 * 1024(256KB)是吞吐与延迟平衡点 - 流媒体类文件(视频/音频)可设
1024 * 1024~4 * 1024 * 1024(1–4MB),减少round-trip - 高频随机读写场景(CAD图纸、快照)建议
64 * 1024~128 * 1024(64–128KB) - 必须在初始化
GridFSBucket时传入,例如new GridFSBucket(db, { chunkSizeBytes: 1048576 }),后期无法动态覆盖
WiredTiger参数必须同步调的三项
只改cacheSizeGB对GridFS写入提升有限——它救不了元数据锁、journal刷盘、事务槽位耗尽这三道硬瓶颈。
-
storage.wiredTiger.engineConfig.cacheSizeGB: 设为内存的60%~75%,留足空间给OS文件系统缓存(journal和mmap页更关键) -
storage.wiredTiger.collectionConfig.blockCompressor: MongoDB 6.0+用zstd,比默认snappy压缩率高20%,减小写入字节数和journal体积 -
storage.wiredTiger.engineConfig.journalCompressor: journal必须用zlib(zstd不支持),降低日志I/O压力
真正容易被忽略的是:事务槽位concurrentTransactions.write默认128,在高并发小文件场景下极易打满,后续请求排队等slot——这个值需要结合实际QPS压测调整,不能只盯缓存。











