前两个阶段讲的索引、查询、explain,都停在了「逻辑层」。我们知道了查询怎么走索引、怎么回表,但没回答一个更底层的问题:这些索引和数据,到底在内存里还是磁盘上,查询快慢的真正分界线在哪。
这条分界线就在 WiredTiger 里。MongoDB 的性能抖动——查询突然变慢、写入周期性尖峰、内存涨了又跌——绝大多数都能在 WiredTiger 的 Cache 和 checkpoint 上找到根因。这一阶段(13–17)就下沉到存储引擎,从「数据怎么在内存和磁盘之间流动」的角度,把 MongoDB 的性能地基补完整。
先把机制边界说清楚
WiredTiger 是 MongoDB 3.2 起的默认存储引擎(更早的 MMAPv1 已在 4.0 移除)。它管三样东西:
- B-tree 文件:每个集合、每个索引都是一个独立的
.wt文件,内部按 B-tree 组织成「页」。 - Cache:一块内存缓冲区,缓存最近访问的热页。查询命中 Cache 就快,没命中就要读盘。
- journal(WAL):预写日志,每个写操作先记日志再改页,保证崩溃可恢复。
WiredTiger 的核心设计是「Cache 优先 + 惰性刷盘」:写入先进 Cache 改内存页(变成脏页),脏页不立即写盘,而是攒着,由 checkpoint 周期性地刷成一个一致的磁盘快照。这让写入看起来很快(大部分只动内存),但也埋下了抖动的种子。
数据在 B-tree 页里怎么存
每个集合和索引在 WiredTiger 里都是一棵 B-tree,B-tree 的节点是「页」。页是 WiredTiger 的最小读写单位,默认大小有 4KB、8KB、16KB、32KB 几档(可配)。即使你只读一个字段,也要把整个页加载进内存。
这点很重要,它解释了几个现象:为什么文档越紧凑查询越快(一页能装更多文档,一次 IO 读到更多数据);为什么大文档会让性能下降(一个文档占大半页,浪费页空间);为什么索引页通常比数据页热(查询总要先走索引)。
页在磁盘上是压缩存储的(默认 snappy),加载进 Cache 时解压。所以「磁盘占用」和「Cache 占用」不是一回事——一个 1GB 的集合文件,解压后可能占用更多 Cache。
Cache:性能的分水岭
Cache 是 WiredTiger 性能的核心。它的大小默认是 max(50%RAM - 1GB, 256MB),这块内存缓存最近访问的集合页和索引页。查询的快慢,本质上就是「命中 Cache 还是读盘」的差别——内存读是纳秒级,磁盘读是毫秒级,差了几个数量级。
Cache 里有两类页:
- 干净页:和磁盘一致的缓存副本,淘汰掉不丢数据。
- 脏页:被写入修改过、还没刷盘的页。脏页必须刷盘后才能淘汰,所以淘汰脏页比淘汰干净页贵。
Cache 满了之后,WiredTiger 要淘汰(evict)冷页腾空间。如果淘汰的都是干净页,代价小;如果被迫淘汰脏页(脏页比例高),要先刷盘再淘汰,会明显增加 IO 和延迟。Cache 压力大的时候,你会看到 pages evicted 飙升、查询延迟跟着抖——这是「工作集超过内存」的典型症状。
判断 Cache 健康的关键指标是 wiredTiger.cache:
bytes read into cache高 → 大量读盘,Cache 命中率低,数据量可能超过内存。pages evicted by application thread高 → 应用线程被迫参与淘汰(本该由后台淘汰线程做),会直接拖慢查询。tracked dirty bytes持续接近上限 → 脏页积压,写入可能被限速。
checkpoint:周期性抖动的源头
Checkpoint 是 WiredTiger 把 Cache 里的脏页刷成一个一致磁盘快照的机制,默认每 60 秒一次。它的作用是缩短崩溃恢复时间:恢复时只要从最近 checkpoint 回放之后的 journal 即可,不用扫全部日志。
但 checkpoint 是有代价的。刷盘期间 IO 会明显上升,如果脏页积压多,这一波 IO 可能让写入出现延迟尖峰。很多 MongoDB 部署里出现的「每隔一分钟一次的写入抖动」,根因就是 checkpoint。
几个相关的调优点:
storage.wiredTiger.engineConfig.checkpointMs:checkpoint 间隔,默认 60000ms。调大能减少刷盘频率,但崩溃恢复要回放更多 journal;调小更频繁但每次更轻。- 脏页比例上限:
wiredTiger.cache.eviction_dirty_trigger,脏页超过这个比例会触发紧急淘汰,限制写入速度。保持脏页比例低,能减少 checkpoint 时的集中刷盘压力。 - 预读和后台淘汰:WiredTiger 有后台淘汰线程,会主动把冷脏页刷盘,避免 checkpoint 时积压。如果后台淘汰跟不上,应用线程会被拉来帮忙,直接拖慢查询。
journal:先记日志再改页
journal 是 WiredTiger 的预写日志(WAL)。每个写操作(insert/update/delete)在改 Cache 里的页之前,先把改动记到 journal。这样即使崩溃,也能从 journal 恢复已提交但还没 checkpoint 的写入。
journal 的刷盘策略由 storage.journal.commitIntervalMs 控制,默认 100ms——也就是写入最多延迟 100ms 才真正落盘。这就是为什么 MongoDB 的写入不是「立即持久」的,而是「最多 100ms 后持久」。配合写关注 w: 1(等 journal 记录)或 w: "majority"(等多数节点确认),能在持久性和性能之间调档。下一篇会专门展开 journal 与持久化。
取舍与边界
WiredTiger 这套设计的优势是「内存优先,写入快」:大部分写操作只动 Cache,journal 追加开销小,所以 MongoDB 的写入吞吐通常很高。代价是:
- 数据超过内存时性能断崖。Cache 装不下工作集,大量读盘,查询延迟飙升。这是 MongoDB 最常见的容量拐点。
- 周期性抖动。checkpoint、journal 刷盘会带来周期性的 IO 尖峰,对延迟敏感的业务要监控和调优。
- 脏页淘汰的隐藏成本。脏页比例高时,淘汰和 checkpoint 都会变贵,写入被限速。
版本演进上,WiredTiger 持续优化了淘汰算法(更智能的 LRU 变体)、压缩(zstd、column compression)和并发(更细粒度的锁)。但「Cache + 页 + checkpoint + journal」这套骨架始终是性能的主战场。
判断框架
- MongoDB 性能抖动,先看 WiredTiger Cache,不要只看 mongod 整体指标。
bytes read into cache高 = 工作集可能超过内存,这是容量问题,不是索引问题。- 周期性写入抖动,先怀疑 checkpoint,再看 journal 刷盘。
- 应用线程参与淘汰(application thread eviction)是红灯,说明后台淘汰跟不上,查询会被拖慢。
- 调优的三个旋钮:Cache 大小、checkpoint 间隔、脏页淘汰阈值。
- 紧凑文档 + 合理页大小,能让一页装更多数据,提升 Cache 效率。
下一篇会深入 Cache 的淘汰机制,讲清楚内存治理的细节。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 MongoDB」,欢迎关注公众号 「十三Tech」。后续会按存储引擎、高可用和分片集群这条线更新。

