先给你一个判断:频繁插入这事儿吧,本身并不会直接产出索引碎片,但它会像翻跟斗一样,让索引页分裂得更频繁,让空间复用变得严重失衡。真正值得盯着的,是 WiredTiger 的 page fill rate 和 a vailable for reuse 空间——这才是诊断碎片的核心指标。

一句话总结就是:频繁插入本身不直接产生索引碎片,但会加速索引页分裂和空间复用失衡——真正要盯的是 WiredTiger 的 page fill rate 和 a vailable for reuse 空间。
为什么 insert 多了索引就变慢?
WiredTiger 引擎在插入时是按 B-tree 结构往索引页里写数据的。当字段基数比较低,比如 status: "pending" 这种,或者写入的顺序跟索引键不匹配,比如虽然按时间戳建的索引,但插入的数据实际上是乱序的,那你就会频繁触发页分裂。分裂之后,旧的页面留下一堆空洞,新的页面又分散地写下去,物理上不连续了,但逻辑上查询还能走——这就是所谓的“索引碎片”。
- 现象很典型:查询用
explain("executionStats")一看,totalDocsExamined远远大于nReturned,同时wiredTiger.block-manager.file bytes a vailable for reuse这个值一直在涨。 - 注意一点:并不是所有高插入量都会导致碎片化,关键在于索引字段的分布是否集中,插入操作是不是有规律地批量进行。
- 设计复合索引时,把高基数字段放前面,比如
{userId: 1, createdAt: -1},比起反过来放{createdAt: -1, userId: 1},能更有效地缓解页分裂。
compact 能不能直接修 insert 导致的碎片?
能用是能用,但有硬性限制。compact 会把整个集合的数据文件和所有索引都重写一遍,释放掉那些 a vailable for reuse 空间,让索引页在物理上连续起来。但它的本职只解决“空间复用不均”,并不能修正索引设计上的缺陷。
- 执行命令时,必须在副本集的 Primary 节点或者分片集群的 shard 节点上操作:
db.runCommand({ compact: "mycollection", force: true }) - 执行期间,该集合的读写操作会全部被阻塞(WiredTiger 会加一个集合级别的排他锁),所以只能挑业务低峰期来做。
- 要是索引本身是建在像自增计数器、毫秒级时间戳这种写密集字段上,那 compact 之后用不了几个小时碎片就会卷土重来——先改索引设计才是正道。
reIndex 和 background: true 建索引有什么区别?
reIndex 是阻塞式重建,而 createIndex({ ..., background: true }) 是后台构建,两者对写入压力的影响逻辑完全不同。
db.mycollection.reIndex():直接锁表,所有写入操作都得排队等着。适合小集合或者有明确维护窗口的场景。background: true:虽然不会锁表,但它仍然需要在内存里构建 B-tree,并且会跟其他操作争抢wiredTiger.cache和 journal 的刷盘带宽。遇上瞬时高并发写入,反而可能让 IO 延迟雪上加霜。- 一个更稳妥的做法是:先把旧索引 drop 掉(
dropIndex),然后用collMod + indexBuildRetry来控制索引构建的节奏,万一失败了还能自动续建,不用从头再来。
日常怎么防住 insert 引发的碎片?
核心思路就两个:控制索引更新的频次,以及控制数据写入的局部性。别等到碎片已经很大了才去动手。
- 像
updatedAt、counter这种写密集的字段,尽量不要单独建索引。如果实在要查,考虑走覆盖查询,配合一个前置过滤字段的复合索引。 - 批量插入之前,可以用
db.runCommand({ setParameter: 1, wiredTigerEngineConfigString: "cache_size=8G" })临时把 cache 调大一点,减少刷盘时的竞争(需要配合wiredTiger.cacheSizeGB配置来用)。 - 要监控
wiredTiger.cache: bytes currently in the cache这个值,如果长期超过 max 的 90%,说明索引节点在频繁换出,碎片的负面影响会被放大很多。 - 用 GridFS 时,要永远用
GridFSBucket.delete()来删除文件,别只删fs.chunks留下孤儿块,这可是fs.chunks.files_id_1_n_1索引碎片最主要的原因。
说到底,碎片不是“坏了”,而是索引结构跟写入模式不匹配的自然结果。最容易被忽略的一个点是:compact 和 reIndex 都只是治标,真正能治本的,是索引字段的选择和写入节奏的控制——尤其是当插入操作来自消息队列或者日志采集这种不可控的源头时,得靠前置聚合或者 TTL 来削峰。
