上一篇把 WiredTiger 的 B-tree 页、Cache 和 checkpoint 三件套铺开了,这一篇聚焦其中最敏感的部分——Cache 的淘汰机制。它是 MongoDB 性能抖动最直接的来源:查询平时很快,突然变慢,往往就是 Cache 淘汰出了问题。

理解 Cache 淘汰,能回答几个线上高频疑问:为什么数据量没变,查询延迟却开始抖?为什么内存明明没用满,mongod 却报 Cache 压力?为什么加内存能立竿见影地稳定延迟?这些都落在 Cache 淘汰这条线上。

先把机制边界说清楚

WiredTiger 的 Cache 是一块固定大小的内存(默认 max(50%RAM - 1GB, 256MB)),缓存集合页和索引页。它不是无限大的缓冲池,而是有「水位线」的:使用率到了一定比例就开始淘汰冷页,腾空间给新页。

关键的水位线有三个:

  • 目标使用率(约 80%):Cache 用到这个比例,后台淘汰线程开始积极工作,把冷页淘汰出去。
  • 脏页触发线:脏页(被修改但没刷盘的页)占比到一定比例(默认约 20%),触发紧急脏页淘汰,限制写入速度。
  • 紧急线(约 95%):Cache 快满了,应用线程也被拉来参与淘汰,查询直接变慢。

这套机制的目标是「保持 Cache 不溢出」,但它有明显的代价:淘汰是要花 CPU 和 IO 的,尤其是淘汰脏页(要先刷盘)。当淘汰压力超过后台线程的处理能力,代价就会传导到应用线程。

Cache 淘汰的三种状态

Cache 与淘汰:内存治理

把 Cache 的使用率分成三段,对应的性能状态完全不同:

健康状态(使用率 < 80%):后台淘汰默默做。 专门的淘汰线程把冷页淘汰出去,干净页直接丢(磁盘有副本),脏页先刷盘再丢。应用线程完全不参与,查询延迟稳定。这是理想状态,前提是工作集能装进 Cache。

压力状态(80%–95%):后台淘汰满负荷。 Cache 接近上限,淘汰线程全力工作。如果淘汰速度跟得上新页进入速度,性能仍可控,只是后台 IO 上升。如果跟不上,就会滑向紧急状态。

告警状态(> 95%):应用线程被拉来淘汰。 这是最坏的情况。后台淘汰处理不过来,WiredTiger 强制让处理查询的应用线程「先淘汰几页再查询」。每个查询都多了一份淘汰开销,延迟直接飙升,P99 抖动。监控指标 pages evicted by application thread 飙高就是这个信号。

干净页淘汰 vs 脏页淘汰

淘汰的代价取决于淘汰的是干净页还是脏页:

干净页淘汰便宜。 干净页和磁盘一致,淘汰掉不丢数据,直接从 Cache 移除即可。大量干净页(读多写少的场景)淘汰压力小。

脏页淘汰贵。 脏页被修改过但还没刷盘,淘汰前必须先写回磁盘。如果脏页比例高,淘汰会变成密集的小写入,IO 压力陡增。这也是为什么写密集场景的 Cache 压力比读密集大得多。

控制脏页比例是 Cache 治理的重点。eviction_dirty_trigger(默认约 20%)控制脏页上限,超过这个比例 WiredTiger 会限制写入速度(写入变慢),强迫脏页先刷盘。这个机制保护了 Cache 不被脏页撑爆,代价是写入吞吐被限速。

为什么应用线程淘汰是红灯

应用线程参与淘汰(application thread eviction),是 Cache 治理失效的明确信号。正常情况下,淘汰是后台线程的事,应用线程只管处理查询。当后台淘汰跟不上,应用线程被迫「边淘汰边查询」,相当于查询的执行链路里硬塞进了一段淘汰工作,延迟必然上升。

几个导致应用线程淘汰的常见原因:

  • 工作集远超 Cache。要访问的数据大部分不在内存,每次查询都要加载新页,Cache 被不断置换,淘汰跟不上。
  • 大量顺序扫描。一次性扫大集合,会把热页挤出去,污染 Cache。这也是为什么 COLLSCAN 不光慢,还会伤及无辜查询。
  • 脏页积压。写密集 + checkpoint 没跟上,脏页比例高,淘汰脏页慢。
  • 后台淘汰线程不足。默认淘汰线程数在极端负载下可能不够(可通过 wiredTiger.engineConfig.evictionThreads 调整,但要谨慎)。

怎么判断和治理 Cache

判断 Cache 健康的核心指标都在 serverStatus().wiredTiger.cache 里:

  • bytes read into cache:从磁盘读入 Cache 的字节数。持续高 = Cache 命中率低 = 工作集超过内存。
  • pages evicted by application thread:应用线程淘汰的页数。非零且高 = 红灯,查询在被拖累。
  • pages currently held in cache:当前 Cache 里的页数。
  • tracked dirty bytes:当前脏页字节数。接近上限 = 写入可能被限速。
  • percentage of bytes read into cache / 命中率:命中率低就是容量问题。

治理 Cache 的手段按优先级:

  1. 让工作集装进 Cache。这是根本解。要么加内存,要么用分片把数据分散到多个节点(每个节点的工作集变小)。
  2. 减少顺序扫描对 Cache 的污染。大批量统计查询走从节点、用 limit、必要时用专门的离线分析集群。
  3. 控制脏页比例。写入分摊,避免突发大批量写入打满脏页;调 checkpoint 间隔让脏页更频繁地小批刷盘。
  4. 调淘汰参数eviction_targeteviction_triggereviction_dirty_target 让淘汰更早、更平滑地启动,而不是攒到紧急线爆发。调这些参数要基于监控数据,不是拍脑袋。

判断框架

  • 查询延迟抖动,第一怀疑对象是 Cache 淘汰,不是索引(索引问题用 explain 查)。
  • bytes read into cache 持续高 = 工作集超过内存 = 容量问题,加内存或分片。
  • pages evicted by application thread 高 = 红灯,应用线程在被拖累。
  • 脏页比例高 → 写入被限速 + 淘汰变贵 → 分摊写入 + 调 checkpoint。
  • 顺序扫描会污染 Cache,统计类查询隔离到从节点或离线集群。
  • 工作集 < Cache 大小,是 MongoDB 性能稳定的根本前提,不满足其他优化都治标不治本。

下一篇讲 journal 和持久化,把「写入怎么不丢」这条线补上。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 MongoDB」,欢迎关注公众号 「十三Tech」。后续会按存储引擎、高可用和分片集群这条线更新。

十三Tech公众号二维码