事务生命周期越长,WiredTiger缓存越容易被快照占满,因MVCC要求保留活跃事务开始时刻所有被修改页的旧版本,无法被LRU淘汰,导致cache overflow、WriteConflict频发及eviction激增。

事务生命周期越长,WiredTiger缓存越容易被快照占满
WiredTiger 的事务实现依赖多版本并发控制(MVCC),每个活跃事务都会在内存中维护一份数据快照。事务持续时间越长、写入量越大,快照占用的缓存页就越多——不是“一份快照”,而是多个版本的 page 同时驻留。例如一个运行 45 秒、修改 50 万文档的事务,WiredTiger 必须保留在该事务开始时刻所有被修改页的旧版本,直到事务提交或回滚。这些页面无法被 LRU 淘汰,直接挤占 bytes currently in the cache 空间。
常见错误现象:WriteConflict 频发、pages evicted by application thread 数值突增、cache overflow 日志反复出现。
- 事务内执行
insertMany80 万条,比拆成 100 个 8000 条的短事务,缓存压力高数倍 - 即使只读事务(如带
snapshot读取),只要未结束,也会锁住对应快照页 - 副本集中的多数写操作(如
majority写关注)会延长事务实际生命周期,进一步拖慢快照清理
索引更新和事务日志共用同一块缓存空间
WiredTiger 不区分“数据页”和“索引页”,所有 B-tree 结构(包括 root/internal/leaf)都统一由同一套缓存管理。事务中每插入一条文档,不仅写入数据页,还要同步更新所有相关索引页——这些索引页同样要加载进缓存,并在事务期间保持可回滚状态。索引越多、字段越宽、复合索引前导字段选择越不精准,产生的索引页修改量就越大。
使用场景:高频写入 + 多字段复合索引 + 事务包裹批量操作 → 缓存迅速被索引变更碎片填满。
- 一个含 5 个字段的复合索引,在事务中插入 10 万文档,可能产生远超 10 万页的索引变更记录
-
db.collection.getIndexes()显示存在createdCollectionAutomatically: true的隐式索引,会加剧缓存争用 - 后台
createIndex(..., { background: true })与事务并发时,会抢夺相同缓存资源,触发更频繁的 eviction
wiredTigerCacheSizeGB 设置过低时,脏页堆积直接阻塞写入
事务提交前,所有修改都是“脏页”(dirty pages)。WiredTiger 默认用独立线程异步刷盘,但当脏页占比超过缓存总量的 20%,就会主动阻塞新写入请求,等待刷盘完成。这个阈值不是软限制——它会真实导致 bulk_write_exception 或连接超时,尤其在云环境(如阿里云 MongoDB 实例)中,wiredTigerCacheSizeGB 被硬性限制为实例内存规格的 60%,更容易触达瓶颈。
性能影响:脏页占比从 15% → 22%,QPS 可能骤降 80%,且延迟毛刺明显(P99 从 12ms 跳到 1.2s)。
- 用
db.serverStatus().wiredTiger.cache["bytes dirty in the cache cumulative"]查看累计脏页量 - 调大
wiredTigerCacheSizeGB是最直接缓解手段,但需确保宿主机/实例总内存余量 ≥ 2GB,否则引发 OS OOM kill - 避免同时调高
eviction=(threads_max=8,threads_min=4)——线程增多会加剧 CPU 竞争,反而降低吞吐
事务与批量写入逻辑耦合导致缓存压力不可控
很多代码把业务批处理逻辑和事务边界混为一谈:用一个事务包住整个 insertMany,再配合 sampleDataList.size() 这类错误条件判断,结果是事务既没及时提交,又没正确分片。这种写法让 WiredTiger 无法预测缓存需求,快照和脏页持续累积,直到某次 insert 触发缓存溢出。
容易踩的坑:
-
i == sampleDataList.size()永远为 false(因sampleDataList始终只含 1 个元素),最后一组数据卡在事务里不提交 - 循环中调用
clear()后重复add(),造成数据丢失,但缓存已提前分配,浪费不可回收 - 未设置
maxTimeMS,长事务在慢查询下无限期挂起,快照长期霸占缓存
真正可控的做法,是让事务边界由写入量或耗时驱动,而不是由列表长度或循环变量决定——比如每 5000 条或每 30 秒强制提交一次,且每次都在新事务中执行 insertMany。


















