聚合内存爆表因mongos归并全量数据导致oom,allowdiskuse仅解决shard本地暂存;预聚合需对齐分片键、控制粒度、降维;应用通过代理函数按时间范围和分片键自动路由;数据一致性靠写时标记+异步补漏+ttl索引。

为什么聚合时内存爆了,加 allowDiskUse: true 还是挂?
因为 allowDiskUse: true 只解决“单节点本地聚合阶段”的磁盘暂存问题,而分片集群里真正卡脖子的是:mongos 把全量原始数据拉到自己内存里做归并。一旦 $group 的 _id 和分片键无关(比如按 user_id 分片,却按 order_date 分组),每个 shard 都得把匹配文档全发给 mongos——哪怕开了磁盘选项,mongos 本身不支持磁盘归并,直接 OOM。
预聚合表怎么建才真有用?
预聚合不是简单地把结果存成一张新集合,关键在「对齐分片键 + 控制粒度」:
- 预聚合集合的分片键必须和原集合一致,例如原表用
{ region: 1, user_id: 1 }分片,预聚合表也得用这个键,否则路由失效、查询变广播 - 聚合字段要能被分片键前缀覆盖,比如按
{ region: "$region", day: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } } }分组,region就是分片键前缀,各 shard 可独立完成子聚合 - 避免预聚合字段含高基数字段(如
user_id),否则文档膨胀、索引失效;改用降维方式,比如先按region + hour聚,再上卷到region + day
如何让应用自动走预聚合表,而不是硬编码判断?
靠聚合管道里的 $facet 或业务层路由逻辑都不够稳。更可靠的做法是封装一个聚合代理函数:
示例(Node.js driver):
function smartAggregate(collection, pipeline) {
const matchStage = pipeline.find(stage => stage.$match);
if (matchStage && matchStage.$match.created_at && isDailyRange(matchStage.$match.created_at)) {
return db.collection('orders_daily_summary').aggregate([
{ $match: { region: matchStage.$match.region } },
{ $project: { _id: 0, region: 1, day: 1, total_amount: 1 } }
]);
}
return collection.aggregate(pipeline, { allowDiskUse: true });
}
要点:
- 只对明确的时间范围(如
$gte+$lt覆盖整日)触发预聚合跳转 - 检查
$match中是否包含预聚合表的分片键字段(如region),缺失就 fallback 到原表 - 不要依赖
$facet做双路查询——它会让两路都执行,白耗资源
预聚合表的数据一致性怎么兜住?
定时任务刷全量太重,监听变更又容易丢。折中方案是「写时标记 + 异步补漏」:
- 插入/更新原表时,用
$set写入一个last_modified_day字段(值为{ $dateToString: { format: "%Y-%m-%d", date: "$updated_at" } }) - 每小时跑一次 job,查出所有
last_modified_day在过去 24 小时内有变更的region,重新聚合对应日期窗口 - 预聚合表加 TTL 索引(如
{ expireAt: 1 }),过期后自动清理,避免脏数据堆积
真正难的不是建表或写脚本,而是确认「哪些聚合模式高频、稳定、可降维」——如果业务天天改统计口径,预聚合反而变成负债。先用 explain("executionStats") 抓出 TOP 3 耗内存聚合,再针对它们建模。











