只读查询会持续加载索引页挤占WiredTiger缓存,根本原因是B-tree预热加载、LRU不区分访问类型、低效索引结构导致internal页反复换入;可通过执行统计、索引分析和冷查询隔离优化。

只读查询看似不修改数据,却会持续把索引页加载进WiredTiger缓存并挤占空间,根本原因是索引页按B-tree结构预热加载、LRU淘汰策略不区分访问类型、以及低效索引结构在只读场景下仍被反复拉入内存。
索引页被预热加载而非按需加载
WiredTiger在首次扫描某个索引时,不是只加载查询用到的叶子页,而是倾向于将整条路径上的root页和internal页一并载入缓存——哪怕你只查一个{status: "done"},它也会把深度为5的B-tree中4个内部节点页全拉进来。
这一步操作起来很简单,但后果严重:这些internal页体积小、复用率低,却长期驻留,直接抬高bytes currently in the cache占比。
如果你的索引tree.depth ≥ 6,或indexDetails.
LRU淘汰机制对只读索引完全失效
WiredTiger使用纯LRU策略淘汰缓存页,不区分该页是刚被insert写入的热数据页,还是三天前被一次find()扫过的冷索引页。
只读查询产生的索引页一旦进入缓存,只要没被再次访问,就会在内存紧张时被优先踢出;但问题在于——它们常被反复踢入又踢出:第一次查询加载→缓存满→被淘汰→第二次相同查询再加载→再淘汰。
【page.faults持续高于50/s】就是这个现象的直接信号,mongostat里net列高getmores也往往同步出现。
复合索引前导字段低频导致深层页反复换入
方法一:检查前导字段实际查询频率
运行db.collection.explain("executionStats").find({category: "news", tag: "breaking"}),观察executionStats.totalKeysExamined与nReturned比值。若比值长期 > 100:1,说明category字段区分度差,B-tree搜索必须深入更多internal页才能定位结果,每次查询都强制加载深层分支页。
方法二:用索引统计定位低效路径
执行db.collection.stats({indexDetails: true}),找到对应索引的indexDetails.
方法三:临时禁用索引验证影响
在测试库中执行db.collection.find({category: "news", tag: "breaking"}).hint({"$natural": 1}).explain("executionStats"),对比IXSCAN与COLLSCAN的executionTimeMillis和totalKeysExamined。若两者相差无几,说明该复合索引在只读场景下已失去价值。
冷查询触发的索引页加载无法被预测或规避
第一步:识别真实冷查询来源
启用慢日志(operationProfiling.mode: slowOp, slowOpThresholdMs: 500),持续采集24小时,然后用db.system.profile.find({ns: "db.collection", millis: {$gt: 500}}).sort({ts: -1})抓取所有慢查询,过滤掉明显业务主路径,聚焦那些每月只跑1–2次的报表脚本或后台校验任务。
第二步:确认其是否命中低频索引
对每条慢查询执行.explain("queryPlanner"),检查queryPlanner.winningPlan.inputStage.keyPattern是否指向一个accesses.ops极低的索引(比如 4。
第三步:隔离冷查询索引依赖
对确认无业务价值的冷索引,执行db.collection.dropIndex("category_1_tag_1");如不能直接删,改用background: true重建为精简版,例如只保留{category: 1},去掉tag字段——因为只读查询中tag筛选实际由应用层二次过滤完成,索引无增益。


















