根本原因是每条小文档插入都触发journal即时刷盘、索引更新和b-tree页面分裂;尤其\_id无序时写放大严重,导致iostat显示w/s高、await>20ms、%util≥95%。

为什么插入大量小文档会让磁盘IO直接拉满
根本原因不是“写得多”,而是“每条都触发一次journal刷盘 + 索引更新 + 页面分裂”。WiredTiger默认每100ms刷一次journal,但w: 1写入会强制触发即时刷盘;每插入一条小文档,就可能引起一次磁盘seek、一次page write、一次B-tree节点分裂——尤其当索引字段无序(比如用ObjectId或随机字符串作_id)时,写放大效应极强。
典型现象:iostat -x 1看到w/s极高、await > 20ms、%util持续95%+;mongostat显示netIn高但insert低,说明请求卡在等待IO确认上。
预分配大文档真能降低IO?什么场景才适用
能,但只对**时间序列类、聚合维度固定、读多写少**的数据有效。本质是把N次随机小写,压缩成1次顺序大写,大幅减少journal记录数、B-tree层级变更和磁盘寻道次数。
- 适用:埋点日志按小时聚合、IoT设备指标按天归档、用户行为统计按client+metric+date建模
- 不适用:需要单条更新/删除的业务数据(如订单、用户资料)、实时查询依赖单文档粒度的场景
- 关键点:必须配合
date或timestamp字段做升序索引首位,否则预分配后插入仍会引发页面分裂
示例结构:{ client: 123, metric: "click", day: "2026-05-11", h0: 42, h1: 57, ..., h23: 31 } —— 一天1条,而非86400条。
批量写入必须配对的三个参数
光用insertMany不够,不控制参数照样IO爆炸。重点看这三项:
-
ordered: false:遇到某条失败不中断后续,避免因单条错误拖慢整批;但需业务层自行处理失败项 -
bypassDocumentValidation: true:跳过schema校验(如果已确保数据合规),省去每次解析开销 -
writeConcern: { w: 0 }:客户端显式传入,不是改连接串里的w=0;Java驱动要写BulkWriteOptions.builder().writeConcern(WriteConcern.withW(0))
注意:w: 0不是“不落盘”,是“不等确认”——数据仍在主节点内存buffer,靠后台线程异步刷journal。若此时宕机且journal未刷出,数据丢失。
为什么调了批量+预分配,IO还是下不去
大概率掉进了两个隐藏坑:
- 索引没动:即使禁用索引插入,之后重建索引时仍会扫全表+排序+写B-tree,瞬间打满IO。建议先
db.collection.dropIndexes(),插入完再createIndex,并用background: true防阻塞 - cacheSizeGB太小:WiredTiger缓存不足时,写操作频繁触发page淘汰和脏页刷盘。检查
db.serverStatus().wiredTiger.cache里dirty是否长期>20%;云数据库务必按实例规格配够内存,本地部署建议设为物理内存的60%
最常被忽略的是:预分配文档+批量写入后,若仍用ObjectId作_id,新文档插入位置仍是随机的,WiredTiger内部仍要频繁分裂page。换成{ _id: { day: "2026-05-11", seq: 123 } }这类有序_id,才能真正实现顺序写。











