写入变慢主因是索引写放大:索引过多、过宽或冗余导致单次插入触发大量随机i/o;需检查索引数量、字段宽度、重复索引,并通过慢日志、serverstatus和explain定位无效索引,删前须验证访问频次、慢日志出现率及是否被特殊查询引用。

写入变慢不是磁盘或CPU问题,很可能是索引写放大
当 db.collection.insertMany() 或批量写入耗时明显增长,而 mongostat 显示 netIn 正常、cpu 未满、queue 无积压,但 diskIO 持续高位且 wtCacheDirty 波动剧烈——这大概率不是硬件瓶颈,而是索引数量与写入模式不匹配导致的写放大。
每个文档插入时,MongoDB(WiredTiger引擎)不仅要写数据页,还要同步更新所有相关索引项。索引越多、字段越宽、复合度越高,单次写入触发的随机 I/O 次数就越多。尤其在 insert 后立刻 update 的场景下,一次逻辑写可能引发 5–10 次物理页刷盘。
- 检查当前集合索引数:
db.collection.getIndexes().length,超过 8 个需警惕 - 查看索引字段宽度:
db.collection.stats().indexDetails中关注size和count比值,>2KB/索引项说明字段冗余或嵌套过深 - 确认是否有重复功能索引:比如同时存在
{a:1}和{a:1, b:1},前者对写入纯属冗余开销
如何快速定位“无效索引”对写入的影响
不要只看 explain("executionStats") 的读性能,要查写路径实际负担。关键指标藏在慢写日志和 WiredTiger 统计中:
- 开启写入慢日志:
db.setLogLevel(1, "write"),再观察update或insert操作是否频繁出现"numYields"> 5 或"writeConflicts"> 0 - 对比相同写入负载下,
db.serverStatus().metrics.operation.write中的scanned字段是否异常升高(说明索引更新本身触发了额外扫描) - 运行:
db.collection.explain("executionStats").updateOne({x:1}, {$set:{y:2}}),若返回中"nMatched":1但"nModified":0且"keysExamined"> 100,说明索引选择逻辑干扰了写路径
删索引前必须验证的三个条件
盲目删索引可能让查询雪崩。以下任一条件不满足,就别动索引:
- 该索引在最近 7 天慢日志中
"planSummary"出现次数为 0(用db.adminCommand({getLog:"slow"})抽样验证) - 该索引的
"accesses.ops"值(通过db.collection.stats().indexDetails["name"].accesses.ops获取)低于写入总次数的 0.1% - 该索引字段未被任何
$text、$geoWithin或分片键引用(sh.status()可查分片键)
重建索引比删除更安全,但要注意时机
如果发现多个索引共用前导字段(如 {a:1,b:1}、{a:1,c:1}、{a:1}),直接删掉后两个不如建一个覆盖型索引:{a:1, b:1, c:1}。但重建必须避开业务高峰:
- 使用
background:true参数,避免阻塞读写:db.collection.createIndex({a:1,b:1,c:1}, {background:true}) - 重建期间监控
wtCacheDirty是否持续 > 80%,若是,立即中止并调小wiredTigerCacheSizeGB - 阿里云/腾讯云 MongoDB 实例上,重建索引会触发磁盘 IOPS 瞬时峰值,建议提前 2 小时在控制台观察历史
IOPS 使用率曲线,选波谷时段操作
索引写放大最隐蔽的地方在于:它不会报错,也不会触发告警,只会让每次写入多花几毫秒——而这些毫秒在高并发下会指数级放大成排队、超时、连接池打满。真正要盯住的不是“有没有索引”,而是“这个索引此刻是否还在被业务代码真实调用”。











