上一篇把 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 的使用率分成三段,对应的性能状态完全不同:
健康状态(使用率 < 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 的手段按优先级:
- 让工作集装进 Cache。这是根本解。要么加内存,要么用分片把数据分散到多个节点(每个节点的工作集变小)。
- 减少顺序扫描对 Cache 的污染。大批量统计查询走从节点、用
limit、必要时用专门的离线分析集群。 - 控制脏页比例。写入分摊,避免突发大批量写入打满脏页;调 checkpoint 间隔让脏页更频繁地小批刷盘。
- 调淘汰参数。
eviction_target、eviction_trigger、eviction_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」。后续会按存储引擎、高可用和分片集群这条线更新。

