WiredTiger缓存吃紧时索引页总被换出,因其采用纯LRU淘汰策略,不区分索引页与数据页,长期未访问的索引页(如深层B-tree分支页或低频复合索引页)会被优先踢出,导致频繁page.faults和查询变慢。

WiredTiger缓存吃紧时索引页为什么总被换出
因为WiredTiger按需加载索引页,但淘汰策略是纯LRU(最近最少使用),不区分“索引页”和“数据页”。只要某个索引页长时间没被查询访问,哪怕它体积小、结构关键,也会被优先踢出缓存。内存不足时,bytes currently in the cache持续逼近maximum bytes configured,触发激进淘汰——此时高频查询用到的索引前缀页可能还在,但范围扫描涉及的深层B-tree分支页、或低频字段组合的复合索引页,就很容易反复进出内存。
常见错误现象:
- 同一查询第一次慢、后续快(冷启动后页加载进缓存)
-
mongostat持续显示高page.faults(每秒缺页中断) -
db.collection.explain("executionStats")中totalKeysExamined很大,但nReturned很小,且executionTimeMillis明显偏高
如何设置storage.wiredTiger.engineConfig.cacheSizeGB才合理
不能只看服务器总内存,得扣掉操作系统、文件系统缓存、其他进程、以及MongoDB自身连接线程开销。生产环境推荐值是:系统可用内存的 50%–60%,且必须为整数(WiredTiger不接受带小数点的cacheSizeGB配置,如4.5会报错)。
实操建议:
- 查可用内存:
free -m | awk '/Mem:/ {print $7}'(单位 MB) - 计算目标值:比如可用内存 12GB,取 60% → 设为
cacheSizeGB: 7(向下取整更稳) - 配置路径:
/etc/mongod.conf中修改 storage 段,确保缩进严格(YAML 对空格敏感) - 重启生效:
sudo systemctl restart mongod,再用db.serverStatus().wiredTiger.cache["maximum bytes configured"]验证是否加载成功
调完cacheSizeGB后还要盯哪几个关键指标
光设了上限没用,得看它实际怎么用。重点不是“用了多少”,而是“用得有多紧、多脏”。
必须定期检查的三项:
-
bytes currently in the cache/maximum bytes configured> 95% → 缓存已满,eviction 线程开始抢用户线程资源 -
bytes dirty in the cache cumulative占比超过 20%(即 dirty / current > 0.2)→ 脏页堆积,写入阻塞风险高 -
pages evicted by application threads非零 → 用户线程被迫参与淘汰,说明后台eviction线程已跟不上
这些字段全在 db.serverStatus().wiredTiger.cache 返回里,别只盯着 workingSetPages——那只是估算页数,不是真实内存压力信号。
为什么加了索引、设了cacheSizeGB,索引还是不常驻
索引常驻的前提是“被真正访问过”,而不是“建好了就自动加载”。WiredTiger不会在启动时预热全部索引页,它只响应查询路径加载对应B-tree层级的页。如果查询条件没命中索引前缀(比如对复合索引 {"a": 1, "b": 1} 只查 {"b": 2}),那整个索引页根本不会被触碰。
容易被忽略的点:
- 覆盖查询(
find({a:1}, {a:1,b:1,_id:0}))能减少数据页加载,但不会让索引页“更常驻”,只是降低整体IO压力 - 即使索引页在缓存里,若查询频率低、间隔长,照样会被LRU淘汰——没有“索引特权”
- 大量写入会生成脏页,挤占干净页空间,间接导致索引页被换出(尤其当
dirty比例高时)
真正要做的,是结合业务查询模式,把最热的索引路径跑熟,再配合足够缓存,才能让关键页稳定驻留。


















