反范式化导致内存耗尽:单文档过大(如8MB订单)独占缓存,嵌套数组膨胀工作集,深层索引与事务快照进一步挤占内存,最终引发频繁淘汰、磁盘重读乃至OOM。

反范式化文档过大,直接吃满 WiredTiger 缓存
WiredTiger 的 cacheSizeGB 是按单个文档完整加载进内存来计费的,不是按字段或修改量。一个反范式化后膨胀到 8MB 的订单文档(含嵌套用户信息、地址、100 条商品明细),只要被读一次或参与一次写操作,就独占 8MB 缓存空间。如果高频访问这类文档,bytes currently in the cache 很快逼近 maximum bytes configured,触发频繁淘汰和磁盘重读。
嵌套数组无节制增长,让工作集远超物理内存
反范式化常伴随“把所有相关数据塞进一个文档”的做法,比如日志类集合里用 events: [{ts: ..., data: {...}}, ...] 存上千条事件。这种设计导致单文档体积失控,而 MongoDB 无法只缓存其中某几个子对象——整个文档必须常驻缓存才能支持快速查询。结果就是:实际活跃字段可能只有 5%,但 100% 文档体积都占着内存。
- 工作集(Working Set)= 高频访问文档体积 × 数量,它由业务访问模式决定,但反范式化会人为放大分子
- 当工作集 >
cacheSizeGB× 0.8 时,pages evicted by age显著上升,faults/s持续 > 100,说明内存已成瓶颈 - 使用
db.collection.stats()查看avgObjSize,若 > 2MB 且count> 10 万,就要警惕
索引爆炸进一步挤占缓存空间
为加速反范式化文档里的深层字段查询(如 "items.sku" 或 "user.profile.tags"),不得不建多层嵌套索引。每个索引项本身也要进缓存,且索引大小 ≈ 文档数量 × 索引键平均长度。一个 items.$.sku 单字段索引,在百万级文档中轻松占掉 1–2GB 缓存。
- 用
db.collection.getIndexes()检查是否有"key" : {"items.sku" : 1}这类路径过深的索引 -
db.serverStatus().wiredTiger.cache["bytes belonging to page images in the cache"]包含索引页,该值持续 > 70%maximum bytes configured就说明索引正在抢内存 - 复合索引中包含反范式化大字段(如
{"status": 1, "items": 1})是典型浪费,items字段根本没必要进索引键
更新局部字段却触发整文档重写,快照内存翻倍
WiredTiger 在事务中更新反范式化文档时,无论只改 status 还是加一条 items,都会为该文档保存一份完整旧版本快照。一个 6MB 的用户档案,每天被 updateOne({$set: {lastLogin: new Date()}}) 更新 100 次,就产生 600MB 快照内存——这还不算并发事务叠加。
- 比对
db.serverStatus().metrics.transaction.currentActive和db.collection.stats().avgObjSize,若前者 × 后者 > 0.7 ×wiredTigerCacheSizeGB× 1024³,快照已在挤压可用缓存 - 避免在事务里对大反范式化文档做高频小更新;改用引用式设计 +
updateMany批量更新轻量字段 - 启用
collMod设置changeStreamPreAndPostImages: {enabled: true}前务必确认文档尺寸,否则 change stream 缓存开销会雪上加霜
bytes currently in the cache 慢慢爬到临界点,然后某次批量导入或报表查询就触发 OOM。真正要盯的不是文档结构美不美观,而是 avgObjSize × qps × readConcern 级别共同决定的内存真实水位。


















