$unwind在副本集也会oom,因其会复制含数组文档为n条,若原始文档含大字段(如content、logs),内存瞬间暴涨;必须在$unwind前执行$project剔除大字段、$match强过滤控制数组长度,并谨慎使用preservenullandemptyarrays。

为什么$unwind在副本集里也会OOM,而不仅是分片集群
很多人以为$unwind内存问题只发生在分片集群的mongos上,其实副本集中单个mongod节点执行聚合时同样会爆内存——因为$unwind本身会复制文档:每条含数组的文档,都会被展开成 N 条(N = 数组长度),中间结果量可能瞬间翻几十倍。如果原始文档带大字段(如content、logs),哪怕只展开一次,内存就撑不住。
$unwind前必须做三件事:投影、过滤、判断空数组
不是加了preserveNullAndEmptyArrays: true就万事大吉。真正防OOM的关键动作都在$unwind之前:
- 用
$project显式剔除所有大字段:{ content: 0, raw_data: 0 },别等后续阶段再删——mongod 会在$unwind前就把整条文档读进内存 - 前置
$match强过滤,把数组长度控制在合理范围:比如{ "tags.2": { $exists: true } }能筛掉数组长度 - 检查是否真需要展开空数组:如果业务逻辑允许跳过
tags: []或tags: null的文档,就别设preserveNullAndEmptyArrays: true——它会让mongod为每个空数组生成一条null占位文档,反而增加内存压力
连续$unwind必须配$group收口,且不能漏掉_id路径
嵌套数组(如comments.replies)要两次$unwind,中间不收口就会指数级膨胀。常见错误是只写{ $unwind: "$comments" }和{ $unwind: "$replies" },却没意识到第二次$unwind实际路径是"$comments.replies"。
正确做法是:
- 第一次$unwind后立刻
$group收拢,按根文档_id聚合成数组:{ _id: "$_id", comments: { $push: "$comments" } } - 再对
comments数组$unwind,路径写全:{ $unwind: "$comments.replies" } - 若最终要还原层级结构,收口
$group时必须用完整路径拼_id,例如:{ _id: { commentId: "$comments._id", replyId: "$comments.replies._id" } },漏字段会导致nReturned: 0
副本集没有mongos,但allowDiskUse仍要显式传
副本集查询走的是主节点(primary),聚合全程由单个mongod执行,但它仍有默认100MB内存限制。一旦$unwind后接$sort或$group,照样报Exceeded memory limit for $sort。
解决方法很简单,但常被忽略:
- Node.js驱动中必须写:
collection.aggregate(pipeline, { allowDiskUse: true }),不能只在pipeline里塞{ allowDiskUse: true } - Shell中:
db.coll.aggregate(pipeline, { allowDiskUse: true }),注意这是第二个参数,不是pipeline成员 - Python PyMongo同理:
collection.aggregate(pipeline, allow_disk_use=True) - 关键点:
allowDiskUse: true只对当前mongod有效,副本集里不用考虑“转发”问题,但也意味着你没法靠分片下推来分摊压力——所有内存和磁盘开销都压在primary上
最容易被忽略的是:即使开了allowDiskUse,如果临时目录(默认/tmp)在内存盘(tmpfs)上,或者空间不足,mongod会静默回退到内存处理,OOM照旧。查日志看有没有No space left on device或Failed to create temp directory。











