MongoDB 默认使用 WiredTiger 存储引擎,它在内存中维护一块缓存来保存压缩后的数据页和索引页。当物理内存不足以容纳高频访问的数据集,也就是工作集时,WiredTiger 会频繁将页面写回磁盘、再按需读取,这种缓存交换会让查询延迟逐步放大。处理性能下降时,建议先定位是缓存命中不足、查询扫描量过大,还是实例物理内存真的不够,然后再决定调整参数、优化索引或是扩容。

一、MongoDB的内存都消耗在哪些地方
MongoDB 的内存占用并不能简单等同于缓存用量。对于 WiredTiger 存储引擎来说,最主要的常驻内存区域是写缓存,它同时承担数据页和索引页的读入、淘汰与脏页写回。默认情况下,该缓存大小约为max((物理内存 - 1GB) / 2, 256MB)。例如一台 32GB 内存的服务器,默认缓存大约为 15.5GB,其余内存则由操作系统文件缓存、mongod 堆内对象、聚合与排序临时空间以及连接栈等共同使用。
当工作集小于缓存容量时,大多数读取请求可以直接命中内存,磁盘 IO 很低,延迟自然稳定。一旦工作集超过缓存,WiredTiger 就开始淘汰旧页以腾出空间。如果淘汰的页面随后又被查询重新读入,就会形成反复换页,表现为磁盘读放大和查询变慢。因此内存不足造成的性能下降,本质上不是单纯的内存使用率高,而是缓存已经无法覆盖当前业务真正需要反复访问的数据。
另一个容易被忽略的问题是操作系统交换分区。如果缓存设置过大,或者物理内存本来就紧张,Linux 可能将部分内存页交换到磁盘,这时即使 WiredTiger 认为数据还在缓存中,实际读写也可能经过 swap,性能会急剧恶化。所以处理内存问题时,既要关注 mongod 内部指标,也要观察系统层面的 swap 使用情况。
二、通过指标确认内存不足是否拖慢查询
判断内存瓶颈最直接的方式是查看 WiredTiger 缓存统计。可以在 mongo shell 中执行以下命令:
db.serverStatus({ wiredTiger: 1 }).wiredTiger.cache
返回结果中需要重点观察 bytes currently in the cache 和 maximum bytes configured。前者是当前已经使用的缓存字节数,后者是缓存上限。如果使用率长期超过 80%,并且 tracked dirty bytes in the cache 也维持在高位,说明缓存不仅装得满,还有大量未落盘的脏数据等待写入,此时磁盘压力会明显增加。
另一个关键指标是 pages evicted 和 pages read into cache。如果淘汰页面数量持续增长,甚至与应用读入的页面数量相当,说明同一批数据被反复赶出缓存又重新读入,这正是工作集大于缓存容量的典型特征。还可以通过 mongostat 快速观察运行状态:
mongostat --uri=mongodb://127.0.0.1:27017 -n 5
输出中的 dirty 表示脏数据占比,used 表示缓存使用率,faults 表示读取请求中发生缺页的比例。如果 faults 持续较高,或者 dirty 经常超过 20%,就可以基本判定缓存已经不够用。慢查询日志同样能提供线索,通过 db.setProfilingLevel(1, { slowms: 100 }) 开启慢查询采集,再分析扫描文档数远大于返回结果数的语句,往往能找到把大量冷数据读进缓存的操作。
三、先调整WiredTiger缓存参数降低磁盘压力
在确认物理内存仍有余量、但默认缓存容量偏小的情况下,可以调整 storage.wiredTiger.engineConfig.cacheSizeGB 参数。配置示例如下:
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 8
该参数通常需要在配置文件中修改并重启 mongod 实例后生效,因此调整前要提前规划变更窗口。缓存大小的设置原则是给操作系统和其他进程预留足够内存,一般建议不要超过物理内存的 50% 到 60%。例如 16GB 内存的服务器,设置 8GB 缓存比较稳妥;如果同一台机器还运行了其他服务,预留比例还应进一步提高。
还有一种相反的场景:物理内存不足且系统已经开始使用 swap,此时反而要适当调小 cacheSizeGB。这样做看似减少了缓存,却能避免 mongod 与操作系统争抢内存,防止更严重的 swap 抖动。调小缓存后,虽然磁盘读可能增加,但总体延迟通常仍低于内存被交换到磁盘后的表现。因此调优方向必须结合 free -m、vmstat 等系统命令一起判断,不能盲目增大缓存。
四、索引和查询优化是恢复性能的根本
缓存扩容能缓解问题,但如果查询本身扫描了大量不必要的数据,再大的缓存也会被迅速填满。索引的作用是让查询只访问少量索引页即可定位目标文档,避免把无关文档读入缓存。例如按订单状态排序查询时,应提前创建复合索引:
db.orders.createIndex({ status: 1, createdAt: -1 });
有了该索引后,db.orders.find({ status: "pending" }).sort({ createdAt: -1 }).limit(50) 就可以通过索引完成过滤和排序,不再需要把大量待处理文档全部读入内存。使用 explain("executionStats") 查看执行计划时,重点比较 totalDocsExamined 与 nReturned 的比值。如果扫描了十万条文档却只返回十条,说明索引选择或过滤条件不理想,应该继续优化查询条件或建立覆盖索引。
聚合管道中也存在类似问题。应尽量把 $match 放在管道最前面,并配合索引提前缩小数据范围;使用 $project 只保留后续阶段需要的字段;涉及排序时优先借助索引顺序,避免在内存中进行大规模排序。对于持续增长且带有明显时效性的日志或会话数据,可以使用 TTL 索引自动删除过期文档,从源头压缩工作集:
db.sessions.createIndex({ lastAccess: 1 }, { expireAfterSeconds: 3600 });
优化查询和索引不仅能降低缓存压力,还会减少 CPU 和磁盘消耗,是处理 MongoDB 内存不足导致性能下降时最应该优先投入的部分。缓存参数只是兜底,查询路径如果没有收敛,工作集仍然会不断膨胀。
五、物理内存不足时的横向扩展与容量规划
如果已经确认查询和索引没有明显问题,缓存策略也合理,但工作集仍然超过单机内存容量,就需要考虑增加物理内存或通过分片扩展。分片可以将数据分散到多个节点,每个分片只承载一部分数据,从而让单个 mongod 的缓存压力成倍下降。分片前应分析数据冷热分布,优先把高频访问的数据集中到内存配置充足的节点上,而不是简单按片键均分。
容量规划时可以通过 db.collection.stats() 查看集合的 size 和 totalIndexSize,估算常访问文档和索引的总体积。经验上,工作集应尽量控制在缓存容量的 80% 以内,留有空间处理突发流量和脏页写回。对于容器化部署,还要注意 cgroup 设置的内存上限与宿主机实际可用内存不同,应依据容器限制而不是宿主机总内存来制定缓存大小。
长期运行时建议持续监控 wiredTiger.cache 的使用率、pages evicted、dirty bytes 以及磁盘 IO 等待时间。当缓存使用率连续超过 85%,或者淘汰页面数出现陡增,就应提前介入调优。避免等到查询大面积超时、甚至 mongod 被 OOM killer 终止时才处理,此时业务已经受到严重影响。内存不足导致的性能下降本质上是容量与访问模式的失配,只有结合缓存配置、查询优化和横向扩展三方面共同调整,才能让 MongoDB 恢复稳定吞吐。