自 MongoDB 3.2 起,WiredTiger 成为默认存储引擎,取代了早期的 MMAPv1。理解它的内部机制,是排查 MongoDB 写放大、内存暴涨、checkpoint 卡顿等线上问题的前提。本文把 WiredTiger 拆成几块讲透:B-Tree 页结构、MVCC 并发、缓存与淘汰、checkpoint 与 journal、压缩。
场景:一次莫名其妙的内存占用
线上经常遇到这样的现象:MongoDB 进程 RSS 涨到接近物理内存上限,但你设置的 cacheSizeGB 明明只占一半。又或者写入 QPS 不高,磁盘 IO 却周期性地飙升、伴随短暂的延迟尖刺。这些现象几乎都能在 WiredTiger 的架构里找到根因。要解释它们,得先看数据是怎么组织的。
机制一:B-Tree 与页(Page)
WiredTiger 把每个集合(collection)和索引(index)都存成一棵独立的 B-Tree。树的节点叫 page,分内部页(internal page,只存 key 和子节点指针)和叶子页(leaf page,存实际数据)。
关键设计在于"内存中的页"和"磁盘上的页"是两种不同形态:
- 磁盘上的页是只读的、压缩过的、序列化的连续块。
- 内存中的页除了原始数据,还挂着一个 update list(skip list 结构),记录尚未写回磁盘的增量修改。
当你更新一条文档,WiredTiger 不会原地改磁盘块,而是把这次修改作为一个新的版本节点挂到内存页的 update chain 上。这就引出了 MVCC。
机制二:MVCC 与无锁读
WiredTiger 用多版本并发控制(MVCC)实现读写不互斥。每个事务启动时拿到一个快照(snapshot),本质是一组对它可见的事务 ID 范围。读操作沿着 update chain 往下找,跳过那些"在我的快照之后才提交"或"尚未提交"的版本,定位到对当前快照可见的那个版本。
类比一下:update chain 像一摞便利贴按时间叠在文档上,每个读者只能看到自己进门那一刻之前贴上去的便利贴。
这带来一个重要的工程权衡:长事务/长游标会拖住快照。只要某个旧快照还活着,它可能可见的旧版本就不能被清理(WiredTiger 的内部叫 oldest timestamp/snapshot 不能推进),update chain 越积越长,内存吃紧,读取还要遍历更长的链。线上 WT_CACHE_FULL 类的告警,很多时候根因是某处忘了关闭的游标或跑了很久的聚合。
文档级别的写冲突在这里以乐观方式处理:两个事务同时改同一文档,后提交的会拿到 WriteConflict,由上层(MongoDB)透明重试。所以 WiredTiger 标称的"文档级并发"并不是行锁,而是乐观 MVCC + 冲突重试。
机制三:Cache 与 Eviction
WiredTiger 自己管一块缓存,默认大小是 max((RAM - 1GB) * 0.5, 256MB)。注意这只是 WiredTiger 内部缓存——它存的是未压缩的页。而操作系统的文件系统缓存(page cache)另外存压缩后的磁盘块。这正是开头 RSS 超过 cacheSizeGB 的原因:你看到的 RSS = WiredTiger cache + 文件系统缓存 + 堆内存碎片等。
1 | # 查看 WiredTiger cache 使用情况 |
Eviction(淘汰)是把内存页写回磁盘、腾出空间的过程。WiredTiger 有两条阈值线:
1 | eviction_target = 80% 后台 eviction 线程开始干活 |
一旦缓存里的 dirty 数据(eviction_dirty_trigger,默认 20%)或总占用触发 trigger 线,应用线程会被强制参与淘汰,表现就是写入延迟突然抖动。看到延迟尖刺先查 application threads page write from cache to disk 这类指标。
淘汰一个被修改过的页时,会发生 reconciliation:把内存页 + update chain 合并、重新切分(split)、压缩,生成新的磁盘页。这是 WiredTiger 写放大的主要来源——改一条小文档,可能触发整页重写。
机制四:Checkpoint 与 Journal 的持久化分工
WiredTiger 用两套机制保证持久性,分工要分清:
Checkpoint 默认每 60 秒(或 journal 累积到 2GB)做一次,把当时的一致性快照刷到磁盘,形成一个可恢复的稳定点。Checkpoint 本质是一次大的 reconciliation + fsync,这就是周期性 IO 尖刺的来源。
Journal(WAL,预写日志)记录两次 checkpoint 之间的所有写操作。默认每 100ms 或事务提交时(取决于 write concern 的 j 选项)落盘。
1 | crash 恢复流程: |
工程权衡很清晰:checkpoint 间隔越长,journal 重放越久,恢复越慢,但平时的 IO 越平稳;反之恢复快但 IO 抖动频繁。w:majority, j:true 的写关注会强制 journal 同步落盘,延迟换安全。
机制五:压缩
WiredTiger 默认对集合用 block compression(snappy),对索引用 prefix compression(利用 B-Tree 同页 key 前缀高度重复的特性)。可选 zlib/zstd 提高压缩率但更耗 CPU。
1 | db.createCollection("logs", { |
权衡:snappy 解压快、CPU 省,适合热数据;zstd 压缩率高,适合冷数据/日志归档,代价是 CPU。索引的 prefix compression 几乎是白送的收益,因为 key 在 B-Tree 里本就有序。
常见误区与踩坑
- 误区:cacheSizeGB 设成物理内存的 80%。错。WiredTiger cache 之外还要给文件系统缓存留空间(它缓存压缩块,对读性能很关键),默认 50% 是经过权衡的,容器里务必显式设置,否则 WiredTiger 可能读到宿主机内存而超配。
- 踩坑:长游标拖垮 eviction。聚合/快照备份/逻辑导出忘了关游标,oldest timestamp 推不动,dirty cache 涨满,全库写入抖动。监控
cursor.timed out和 cache dirty 比例。 - 误区:以为 WiredTiger 是行锁。它是乐观 MVCC,高并发改同一文档会持续
WriteConflict重试,热点文档(如全局计数器)是性能陷阱,应改用$inc分片计数或拆分热点。 - 踩坑:checkpoint 期间的 IO 尖刺被当成业务问题。先看尖刺周期是不是 60 秒对齐,是的话就是 checkpoint,调整
eviction参数或上更快的盘,而不是去查业务代码。
小结
WiredTiger 的核心是"内存里 MVCC 增量 + 磁盘上压缩 B-Tree",中间用 reconciliation 衔接,用 checkpoint + journal 保证持久。把握三条主线就能定位大多数问题:内存看 cache/eviction、持久看 checkpoint/journal、并发看 MVCC 快照与写冲突。线上调优的第一步永远是看 serverStatus().wiredTiger,而不是盲调参数。