WiredTiger cacheSizeGB设过高反致查询变慢:易引发系统swap、缓存命中率下降、handleLock争抢、脏页堆积及主从切换后缓存未预热等问题,需结合工作集、索引密度与写入负载综合调优。

WiredTiger cacheSizeGB设太高,反而让查询更慢
内存占用高 ≠ 性能好。当 wiredTigerCacheSizeGB 被设为物理内存的 70% 甚至更高,而实例又和其他服务共机(比如 Nginx、Redis),操作系统会开始频繁 swap,MongoDB 线程被调度延迟,find() 响应毛刺明显上升。
真正关键的是“有效驻留”:WiredTiger 缓存里得有你高频查询用到的索引页和热数据页。如果 cache 太大但工作集分散(比如几百个低频集合 + 大量冷索引),缓存命中率反而下降,bytes_read_into_cache 持续走高,说明磁盘在补漏。
- 用
db.serverStatus().wiredTiger.cache查看bytes currently in the cache占maximum bytes configured的比例;长期低于 60%,大概率是 cache 过大或工作集没对齐 - 容器环境必须显式配置
--wiredTigerCacheSizeGB,cgroup 内存限制不会被自动感知,否则 MongoDB 会按宿主机总内存算,默认值可能翻倍超标 - 副本集 Secondary 节点若开启
readPreference=secondary,它的 cache 也得单独调——别直接复用 Primary 的配置值
索引太多导致 handleLock 等待飙升
每个索引对应一个独立的 WiredTiger 文件和内存中的 dhandle 结构。当集合数 × 索引数 > 2000,schemaLock 和 handleLock 争抢会成为瓶颈,哪怕查单条文档,日志里也会出现 timeWaitingMicros: { schemaLock: 134101710 } 这种百毫秒级锁等待。
这不是查询写得差,是引擎层打开资源过多引发的系统级阻塞。尤其在副本集初始化同步、备份恢复、或执行 count() 类操作时,问题会被放大。
- 用
db.adminCommand({listDatabases: 1})+db.getCollectionNames()统计库表总数,再结合db.collection.getIndexes()算索引密度 - 删除长期不用的索引:先
db.collection.dropIndex("name_1_status_-1"),再观察慢日志中storage.timeWaitingMicros.handleLock是否回落 - 避免为每个字段都建单字段索引;优先合并为复合索引,例如把
{a: 1}和{a: 1, b: 1}合并成{a: 1, b: 1, c: 1}(按查询 filter 顺序)
主从切换后索引“不热”,光调 cache 没用
新主节点刚上位时,bytes currently in the cache 可能接近 0,所有查询都要从磁盘加载 B-tree 路径、重建执行计划。此时调大 wiredTigerCacheSizeGB 只是给空缓存划地盘,不解决“热身”问题。
更危险的是手动执行 db.collection.getPlanCache().clear() —— 它会让每个 find() 都重新跑 queryPlanner,对含 $or 或多阶段聚合的查询,计划生成耗时可能占整个响应的 80% 以上。
- 预热不是刷全表,而是模拟真实访问路径:从慢日志提取 top 5 filter+sort 组合,用
.hint()强制走目标索引,例如db.orders.find({ status: "paid" }).sort({ updatedAt: -1 }).limit(1).hint({ status: 1, updatedAt: -1 }) -
.count()和.estimatedDocumentCount()不加载索引页,不能用于预热;改用.findOne().hint() - 确认计划劣化再清理缓存:先开 profiling(
db.setProfilingLevel(1, { slowms: 50 })),查executionStats.planGenerationTime占比是否异常高
cacheSizeGB 和 dirty 数据比例要一起看
WiredTiger cache 不只是“装数据”,它还管理脏页(已修改未刷盘)。当 cacheDirty 持续超 20%,写入线程会被阻塞,连带影响读——因为读请求可能触发强制 eviction(驱逐脏页腾空间),造成卡顿。
这时候单纯加内存或调大 cacheSizeGB 是治标;必须配合清理线程调优,否则 cache 越大,脏页堆积越隐蔽,某次 checkpoint 就可能引发雪崩。
- 检查
db.serverStatus().wiredTiger.cache["bytes dirty in the cache cumulative"],除以总写入量估算脏页占比 - 调高 eviction 线程数(仅限 4.0+ 副本集):
db.runCommand({"setParameter": 1, "wiredTigerEngineRuntimeConfig": "eviction=(threads_max=8,threads_min=4)"}) - 写密集场景下,
cacheSizeGB建议设为写入吞吐 × 2s 的缓冲量,而不是拍脑袋按内存百分比定
真正难处理的,是 cacheSizeGB、索引数量、查询模式这三者之间的耦合关系——调一个参数,另外两个的副作用可能隔几小时才暴露。建议每次只动一项,用 db.serverStatus().wiredTiger.cache 和慢日志里的 storage.timeWaitingMicros 交叉验证效果。











