MongoDB 运行过程中,WiredTiger 缓存内存占用突然飙升,是许多团队常遇到的难题。经过深入排查,十有八九并非参数配置问题,而是文档模型本身埋下了隐患。归根结底,四个最常见的原因:嵌入文档过大、字段冗余、索引膨胀、冷数据滞留。下面逐一剖析。

嵌入式文档过大导致内存压力攀升
一个文档中嵌入了数十个子文档——例如课程文档里塞了四五十个视频、测验、课件——每次操作这个文档,哪怕只是读取其中一个字段,整个文档都必须完整加载进缓存。WiredTiger 的 bytes currently in the cache 记录的是完整文档大小,而非实际用到的数据。这样便造成了缓存污染,内存被白白占用。
如何判断?运行 db.serverStatus().wiredtiger.cache,如果 bytes currently in the cache 长期逼近 maximum bytes configured 上限,但实际热数据占比很低,那基本就是中了这一条。
解决办法其实并不复杂:
• 将高频独立访问的子资源拆解到单独集合中,例如 lessons、quizzes,使用 ObjectId 引用替代内嵌。
• 对嵌入数组设定长度上限,比如 course.lectures: { $size: { $lt: 20 } },并配合应用层分页,防止单个数组膨胀到数百项。
• 切勿在嵌入文档中存储二进制数据(BinData)或长文本(如 Base64 编码的图片),应改用文件服务加 URL 字段,让缓存只保留轻量的引用。
冗余字段和宽松 schema 加剧内存开销
MongoDB 不强制 schema,但若放任字段随意增减,同一集合内文档结构将变得五花八门。WiredTiger 对每个文档单独压缩和缓存——字段名重复存储、字段顺序混乱、类型混用(例如 "status": "1" 和 "status": 1)都会降低压缩率,导致缓存体积增大。
举个例子:10 万条用户文档,如果每条多存一个从不使用的 temp_metadata 字段(平均 200B),仅这一个字段就会额外占用约 20MB 缓存。这些数据根本不会被查询,却长期赖在 LRU 缓存里,挤走了真正需要的热数据。
如何清理?
• 上线前使用 db.collection.aggregate([{$project: {fields: {$objectToArray: "$$ROOT"}}}, {$unwind: "$fields"}, {$group: {_id: "$fields.k", count: {$sum: 1}}}], {allowDiskUse: true}) 统计字段分布,找出低频或废弃字段,直接删除。
• 统一数值字段类型,全部采用 int32 或 int64,避免混用 double。
• 字符串字段启用 collation 并做预处理(trim、小写化),减少因大小写或空格差异导致的索引碎片和缓存抖动。
索引字段膨胀间接推高内存占用
索引本身也需要驻留在 WiredTiger 缓存中。复合索引字段越多、值越长(比如将 user.profile.bio 这种长文本字段塞进索引键),索引条目的体积就越大。更隐蔽的是:如果索引键包含高基数字段(如 timestamp),会导致 B-tree 分支变深、页分裂频繁,进一步放大内存占用。
如何排查?先用 db.collection.getIndexes() 查看,再用 db.collection.stats().indexDetails 查看每个索引的实际大小。经常会发现 30% 以上的索引从未被 explain("executionStats") 中的 executionStages.inputStage.indexName 引用过——这些就是白占内存的元凶。
优化建议:
• 删除从未被 explain() 命中过的索引,尤其警惕以 _id 开头的冗余复合索引。
• 使用部分索引过滤掉无效数据:db.logs.createIndex({level: 1, timestamp: -1}, {partialFilterExpression: {level: {$in: ["ERROR", "WARN"]}}}),这样只有需要的数据才会纳入索引。
• 对于长字符串字段建立索引时,考虑使用 text 索引替代字段前缀索引,或者对原始内容计算哈希(sha256(content)),用哈希字段代替原文。
忽略 TTL 和归档策略导致冷数据滞留内存
日志、事件、临时会话这类数据,如果没有 TTL 索引或归档机制,会长期堆积在集合中。它们虽然很少被查询,但只要被读取过一次,就会挤入缓存。而 WiredTiger 的 LRU 策略并不区分“业务冷”和“系统冷”——结果就是热数据被频繁踢出,冷数据却赖着不走。
在分片集群中,这个问题尤为突出。某些 shard 上冷数据占比超过 70%,但 db.serverStatus().wiredtiger.cache 显示的缓存使用率依然高达 90%+——内存被无效数据低效占用,热数据反而得不到保障。
该怎么做?
• 对于明确生命周期的数据添加 TTL 索引:db.sessions.createIndex({createdAt: 1}, {expireAfterSeconds: 3600}),让 MongoDB 自动清理过期数据。
• 对历史数据定期导出,然后清空旧分片:db.old_logs.renameCollection("old_logs_archived_2025_q2"),或直接 drop 掉不再需要的集合。
• 在应用层写入时主动标记冷热标签,比如 {hot: true, ttl: ISODate("...")},配合后台 job 将冷数据批量迁移到归档库。
其实,真正卡住内存优化效果的,往往不是配置参数调得不够精细,而是数据模型里那些“看起来无害”的嵌套层级、字段冗余和冷热混存——它们不会报错,却持续拖慢缓存效率,而且在监控指标中隐藏得极深。从根源上优化文档模型,比事后调参要管用得多。
