MongoDB 聚合溢出(Spilling)诊断指南:慢查询日志中的 Spilling 统计与 Golden Test 验证
发布时间:2026/9/13 13:18:15来源:尧图网络
MongoDB 聚合溢出Spilling诊断指南慢查询日志中的 Spilling 统计与 Golden Test 验证【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读本文以 MongoDB 仓库中jstests/query_golden/expected_output/internalEnableJoinOptimization/logs_spilling.md这份 Golden Test 期望输出为骨架系统讲解聚合管道在执行$sort、$group、$lookup、$graphLookup、$setWindowFields等内存敏感阶段时数据溢出spilling到磁盘后如何以统计字段的形式出现在慢查询日志Slow query log中。读完本文你将掌握 spilling 统计字段的命名规律与含义、通过内部 Server 参数强制触发溢出的调试方法、Golden Test 的验证机制以及如何从日志中定位聚合内存压力。一、背景什么是聚合阶段的 SpillingMongoDB 聚合框架中有多个阶段需要将中间结果保存在内存中排序$sort、分组$group、桶自动分箱$bucketAuto、哈希连接$lookup的哈希实现、图查询$graphLookup、窗口函数$setWindowFields、文本查询的 OR 合并$text匹配等。当这些阶段的中间数据量超过内存阈值时执行引擎会把部分数据临时写入磁盘处理完成后再读回这一过程称为spilling溢出/落盘。spilling 是 MongoDB 应对内存放不下时的兜底手段它保证查询不会因为内存不足而失败但代价是引入额外的磁盘 I/O显著拉长查询耗时。因此spilling 是否发生、溢出多少次、溢出了多少记录和字节是诊断聚合查询性能问题的关键指标——这也是 MongoDB 将这类统计写入慢查询日志的原因。在jstests/query_golden/logs_spilling_md.js的注释中明确指出本测试的意图Tests the spilling statistics are a part of slow query logs验证 spilling 统计是慢查询日志的组成部分。二、Golden Test 验证机制期望输出如何产生logs_spilling.md并非手写的文档而是由测试运行时自动生成的期望输出expected output。其生成与校验流程记录在驱动测试 jstests/query_golden/logs_spilling_md.js 中核心步骤如下开启 profile 与全局日志测试调用db.setProfilingLevel(1, {slowms: -1})将 profiling 阈值设为-1即每条命令都算慢查询从而保证每条 aggregate 命令都会产生 Slow query 日志。执行聚合并打上注释outputPipelineAndSlowQueryLog()对目标集合执行coll.aggregate(pipeline, {comment: comment})通过comment字段为每次聚合打上唯一标识如one sort、$lookup。从全局日志精确抓取日志行调用db.adminCommand({getLog: global})读取内存日志再用findMatchingLogLine()来自 jstests/libs/log.js按msg: Slow query、comment与command: aggregate三个条件过滤避免误抓子操作产生的日志。提取 spilling 统计并归一化getSpillingAttrs()遍历日志行attr对象按命名规律分类——usedDisk、以Spills/SpilledRecords结尾的字段原样保留数量稳定可作为黄金值以SpilledBytes/SpilledDataStorageSize结尾的字段替换为占位符X字节数随运行环境波动不适合做精确断言spillStorage子对象中的键除data外参见代码中TODO SERVER-109672同样归一化为X。输出 Markdown借助 jstests/libs/query/pretty_md.js 的section/subSection/code/linebreak工具将Pipeline与Slow query spilling stats必要时还有Slow query spill storage stats渲染成标准 Markdown与期望文件比对。该测试带有requires_persistence、requires_fcv_81、requires_profiling标签意味着它要求持久化存储、FCV ≥ 8.1且当前环境支持 profiling。测试结束时finally块会逐一恢复被改动的 Server 参数与 profiling 级别保证不影响其他用例。三、Spilling 统计字段命名规律与源码映射从logs_spilling.md中 14 组输出可以看出明确的命名约定统计字段以触发溢出的阶段名为前缀后接指标名字段模式含义示例stageSpills溢出到磁盘的次数稳定的黄金值sortSpills : 5stageSpilledRecords溢出涉及的记录总数稳定的黄金值sortSpilledRecords : 8stageSpilledBytes溢出写入的字节数测试中归一化为XsortSpilledBytes : XstageSpilledDataStorageSize溢出数据在磁盘存储中占用的空间归一化为XsortSpilledDataStorageSize : XusedDisk本次聚合是否使用了磁盘布尔值usedDisk : truespillStorage溢出存储子对象的统计如等待磁盘 I/O 的微秒数{ timeWaitingMicros : X }文档中出现的阶段前缀包括sort、group、textOr、bucketAuto、hashLookup、graphLookup、setWindowFields。这些字段在源码中有直接的序列化实现。以排序为例src/mongo/db/pipeline/document_source_sort.cpp 中在序列化统计时mutDoc[usedDisk] stats.spilledRanges() 0Classic 执行路径按溢出区间数判断是否落盘mutDoc[spills] stats.spilledRanges()、mutDoc[spilledBytes] _sortExecutor-spilledBytes()、mutDoc[spilledRecords] ...另一路径从spillingStats与排序执行器取数。同样spilledDataStorageSize在 src/mongo/db/exec/agg/bucket_auto_stage.cpp、src/mongo/db/exec/agg/graph_lookup_stage.cpp 等处均通过spillingStats.getSpilledDataStorageSize()输出。而底层落盘容器实现在 src/mongo/db/pipeline/spilling/spillable_deque.cpp 与 src/mongo/db/pipeline/spilling/spillable_map.cpp它们负责把内存中的双端队列/哈希表按需换页到磁盘。四、如何强制触发 Spilling内存阈值 Server 参数全表驱动测试通过把各阶段的内存上限压到极小值通常为 1 字节来强制溢出。下表汇总了测试用到的全部 Server 参数及其作用阶段均通过setParameter管理Server 参数影响的阶段/场景测试中的触发值internalQueryMaxBlockingSortMemoryUsageBytes阻塞排序$sort含多排序、时间序列排序1000不溢出/ 1强制溢出internalDocumentSourceGroupMaxMemoryBytesClassic 执行引擎的$group内存上限1internalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpillSBE 哈希聚合$group溢出阈值1internalTextOrStageMaxMemoryBytes$text查询的 OR 合并阶段需ExtendedAutoSpilling特性标志启用1internalDocumentSourceBucketAutoMaxMemoryBytes$bucketAuto1internalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpillSBE 哈希$lookup1internalDocumentSourceGraphLookupMaxMemoryBytes$graphLookup1internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpillSBE 哈希连接$lookup$unwind1internalDocumentSourceSetWindowFieldsMaxMemoryBytes$setWindowFields1SBE/ 500Classic见下文注意两点细节TextOr 场景有条件测试代码先通过FeatureFlagUtil.isPresentAndEnabled(db, ExtendedAutoSpilling)判断特性标志是否启用只有启用时才执行第 7、8 节并保存/恢复internalTextOrStageMaxMemoryBytes。SetWindowFields 的阈值需探测测试先用coll.explain().aggregate(pipeline)检查执行计划中是否存在WINDOW阶段getPlanStage(getWinningPlanFromExplain(explain), WINDOW)来判断$setWindowFields是否下推到了 SBE。若下推到 SBE 则阈值可设为 1 字节否则保持 Classic 路径此时若 1 字节无法容纳DocumentSourceSetWindowFields在溢出后仍不满足内存限制会直接报错因此 Classic 下取 500。生产环境中若要复现或验证 spilling可在测试实例上执行例如db.adminCommand({setParameter: 1, internalQueryMaxBlockingSortMemoryUsageBytes: 1})然后运行聚合并观察慢查询日志。注意这些均为内部参数修改后应立即恢复以免影响其他操作。五、14 个测试场景逐一解读继承期望输出全文以下逐节重现logs_spilling.md的全部内容并结合驱动测试说明每个场景的数据构造与观察点。场景 1Sort with large memory limit大内存上限排序内存上限设为 1000 字节集合仅 3 条{a: 1..3}文档排序完全在内存中完成。Pipeline[ { $sort : { a : 1 } } ]Slow query spilling stats{ }统计为空对象说明没有发生任何溢出。这是健康基线usedDisk不出现、各*Spills字段缺失。场景 2Sort with empty collection空集合排序对空集合执行同样的排序同样没有溢出。Pipeline[ { $sort : { a : 1 } } ]Slow query spilling stats{ }场景 3Sort with spilling强制溢出的排序将internalQueryMaxBlockingSortMemoryUsageBytes压到 1 字节同样的 3 条文档触发 5 次溢出、涉及 8 条记录排序中间结果被反复换页。Pipeline[ { $sort : { a : 1 } } ]Slow query spilling stats{ sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 8, sortSpills : 5, usedDisk : true }usedDisk: true与sortSpills: 5是判断排序落盘最直接的证据字节数与存储大小因平台波动黄金测试中以X占位。场景 4Multiple sorts管道内多个排序数据为 3 条{a, b}组合文档管道先按a升序、$limit 3、再按b升序两个排序都溢出。Pipeline[ { $sort : { a : 1 } }, { $limit : 3 }, { $sort : { b : 1 } } ]Slow query spilling stats{ sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 16, sortSpills : 10, usedDisk : true }可以看到统计中只有一个sort*前缀说明同一阶段类型这里两个$sort共用sort前缀的多次溢出被汇总计数。场景 5Timeseries sort时间序列集合排序通过createCollection(..., {timeseries: {timeField: time, metaField: meta}})创建时间序列集合并按bucketMaxSpanSeconds / 10的间隔插入 50 条{time, meta: 1}文档按time排序时对底层 bucket 进行排序并全部溢出。Pipeline[ { $sort : { time : 1 } } ]Slow query spilling stats{ sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 50, sortSpills : 50, usedDisk : true }50 条记录对应 50 次溢出说明在 1 字节上限下每条记录都被单独换页是最极端的溢出形态。场景 6Group分组 排序数据为 4 条{a: 1|2, b: 1|2}文档$group按a分组求和再按b排序。同时压低internalDocumentSourceGroupMaxMemoryBytes与internalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpill使 Group 与 Sort 双双溢出。Pipeline[ { $group : { _id : $a, b : { $sum : $b } } }, { $sort : { b : 1 } } ]Slow query spilling stats{ groupSpilledBytes : X, groupSpilledDataStorageSize : X, groupSpilledRecords : 4, groupSpills : 4, sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 4, sortSpills : 3, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }本场景首次出现Slow query spill storage stats小节spillStorage子对象中的timeWaitingMicros表示等待溢出存储磁盘 I/O的微秒时间归一化为X。同时group*与sort*两组字段并存证明一个管道内多个不同阶段可以各自记录溢出。场景 7TextOr and projection文本查询 投影数据为green tea/black tea/black coffee三条文档并在a字段建文本索引$text搜索black tea后用$addFields注入textScore元数据。仅当ExtendedAutoSpilling特性标志启用时才执行内存上限 1 字节。Pipeline[ { $match : { $text : { $search : black tea } } }, { $addFields : { score : { $meta : textScore } } } ]Slow query spilling stats{ textOrSpilledBytes : X, textOrSpilledDataStorageSize : X, textOrSpilledRecords : 4, textOrSpills : 4, usedDisk : true }textOr前缀对应文本查询的 OR 合并阶段TextOr 算子说明$text内部的多 term 合并也会溢出。场景 8TextOr and sort文本查询 按评分排序同样的文本搜索随后按textScore元数据排序$sort的键使用{$meta: textScore}。Sort 与 TextOr 同时溢出。Pipeline[ { $match : { $text : { $search : black tea } } }, { $sort : { _ : { $meta : textScore } } } ]Slow query spilling stats{ sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 8, sortSpills : 5, textOrSpilledBytes : X, textOrSpilledDataStorageSize : X, textOrSpilledRecords : 4, textOrSpills : 4, usedDisk : true }场景 9BucketAuto自动分箱$bucketAuto按a分成 2 个桶并输出sum对b求和内存上限 1 字节触发分箱过程溢出。Pipeline[ { $bucketAuto : { groupBy : $a, buckets : 2, output : { sum : { $sum : $b } } } } ]Slow query spilling stats{ bucketAutoSpilledBytes : X, bucketAutoSpilledDataStorageSize : X, bucketAutoSpilledRecords : 13, bucketAutoSpills : 7, usedDisk : true }场景 10HashLookup哈希$lookup$lookup以name为连接键关联logs_spilling_md_students集合8 条学生文档通过internalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpill: 1强制哈希构建表溢出。Pipeline[ { $lookup : { from : logs_spilling_md_students, localField : name, foreignField : name, as : matched } } ]Slow query spilling stats{ hashLookupSpilledBytes : X, hashLookupSpilledDataStorageSize : X, hashLookupSpilledRecords : 14, hashLookupSpills : 16, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }场景 11Graph lookup图查询$graphLookup以_id: 1为起点、沿to - _id遍历 7 条节点文档depthField: depth记录深度内存上限 1 字节。Pipeline[ { $limit : 1 }, { $graphLookup : { from : coll, startWith : 1, connectFromField : to, connectToField : _id, as : path, depthField : depth } } ]Slow query spilling stats{ graphLookupSpilledBytes : X, graphLookupSpilledDataStorageSize : X, graphLookupSpilledRecords : 2, graphLookupSpills : 2, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }场景 12Graph lookup with unwind and sort图查询 展开 排序在场景 11 的$graphLookup之后追加$unwind: $path与按path.depth升序的$sort。本场景下graphLookup*溢出统计与场景 11 一致2/2但排序部分没有出现在统计中。Pipeline[ { $limit : 1 }, { $graphLookup : { from : coll, startWith : 1, connectFromField : to, connectToField : _id, as : path, depthField : depth } }, { $unwind : $path }, { $sort : { path.depth : 1 } } ]Slow query spilling stats{ graphLookupSpilledBytes : X, graphLookupSpilledDataStorageSize : X, graphLookupSpilledRecords : 2, graphLookupSpills : 2, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }场景 13HashLookupUnwind哈希连接 展开$lookup以locationName连接logs_spilling_md_locations集合3 条位置文档随后$unwind并$project裁剪字段。测试还刻意在两侧各建一个包含dummy前缀的复合索引{dummy: -1, locationName: -1}与{dummy: 1, name: -1}为连接优化提供 multikeyness 信息。该场景通过internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill: 1触发哈希连接溢出。Pipeline[ { $lookup : { from : logs_spilling_md_locations, localField : locationName, foreignField : name, as : location } }, { $unwind : $location }, { $project : { locationName : false, location.extra : false, location.coordinates : false, colors : false } } ]Slow query spilling stats{ hashLookupSpilledBytes : X, hashLookupSpilledDataStorageSize : X, hashLookupSpilledRecords : 6, hashLookupSpills : 6, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }场景 14SetWindowFields窗口函数$setWindowFields按a分区、按b排序对分区内b累加求和。其内存阈值在 Classic500与 SBE1路径下不同通过 explain 探测WINDOW阶段决定。本场景实际包含两个管道变体纯$setWindowFields以及其后追加$limit: 1。Pipeline[ { $setWindowFields : { partitionBy : $a, sortBy : { b : 1 }, output : { sum : { $sum : $b } } } } ]Slow query spilling stats{ setWindowFieldsSpilledBytes : X, setWindowFieldsSpilledDataStorageSize : X, setWindowFieldsSpilledRecords : 3, setWindowFieldsSpills : 2, sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 13, sortSpills : 7, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }Pipeline[ { $setWindowFields : { partitionBy : $a, sortBy : { b : 1 }, output : { sum : { $sum : $b } } } }, { $limit : 1 } ]Slow query spilling stats{ setWindowFieldsSpilledBytes : X, setWindowFieldsSpilledDataStorageSize : X, setWindowFieldsSpilledRecords : 2, setWindowFieldsSpills : 1, sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 13, sortSpills : 7, usedDisk : true }Slow query spill storage stats{ timeWaitingMicros : X }注意两个变体都同时携带setWindowFields*与sort*两组统计窗口函数内部隐含的排序sortBy单独计入sort前缀而窗口本身的溢出计入setWindowFields前缀追加$limit后窗口溢出次数从 3/2 降为 2/1说明提前终止优化减少了窗口阶段的数据量。六、执行引擎变体同一测试在不同特性标志下的差异logs_spilling.md并不仅存在于internalEnableJoinOptimization目录。仓库中同一份期望输出还出现在 jstests/query_golden/expected_output/featureFlagSbeFull、jstests/query_golden/expected_output/sbeDisabled、jstests/query_golden/expected_output/sbeFull、jstests/query_golden/expected_output/sbeRestricted 各目录下。同一测试在开启/关闭不同特性标志SBE 全量、SBE 受限、SBE 禁用、内部连接优化等时运行得到各自的期望输出用于回归捕获执行引擎行为差异。这一点在场景 14 的数值上体现得尤其直观internalEnableJoinOptimization变体中纯$setWindowFields的溢出为3 条 / 2 次而sbeFull变体中对应数值为4 条 / 4 次。可见相同管道在不同执行路径下溢出行为不同而 Golden Test 正是用来锁住这些行为防止执行引擎改动时统计悄然漂移。如果你在某个特性标志组合下发现 spilling 统计与期望不符这通常意味着执行计划或溢出策略发生了变化。七、实践如何用 Spilling 统计诊断线上聚合结合文档与源码可以沉淀出以下可落地的排查方法开启慢查询日志中的 spilling 信息慢查询日志默认即包含 spilling 统计无需额外开关可通过setProfilingLevel(1, {slowms: 阈值})调整慢查询门槛或直接用db.adminCommand({getLog: global})检索msg: Slow query与command: aggregate的日志行。按字段定位瓶颈阶段看日志中的*Spills与*SpilledRecords前缀即可知道是哪类阶段在落盘——sortSpills对应$sort/$setWindowFields.sortBygroupSpills对应$grouphashLookupSpills对应$lookupgraphLookupSpills对应$graphLookuptextOrSpills对应$text匹配bucketAutoSpills对应$bucketAuto。usedDisk: true是总开关。评估溢出代价timeWaitingMicrosspillStorage子对象给出了等待溢出存储 I/O 的时间字节数*SpilledBytes、*SpilledDataStorageSize体现落盘数据规模。若溢出频繁且等待时间长说明中间结果远超内存预算。对症优化为$sort/$group的字段建索引以利用索引有序流式处理为$lookup的foreignField建索引以走非阻塞执行路径为$text查询控制$search词项数量必要时调整对应内部内存参数如internalQueryMaxBlockingSortMemoryUsageBytes放宽内存预算——但注意这些是内部参数需评估实例内存余量后再谨慎调整。结合 explain 交叉验证测试中通过coll.explain().aggregate(...)检查WINDOW等执行阶段来判断下推路径生产排查同样可以用explain确认计划形态再与慢查询日志中的溢出统计互相印证。八、总结jstests/query_golden/expected_output/internalEnableJoinOptimization/logs_spilling.md表面上只是一份管道 统计 JSON的期望输出实质上是一份完整的聚合溢出行为规格它定义了 MongoDB 聚合框架中各内存敏感阶段的溢出统计字段*Spills、*SpilledRecords、*SpilledBytes、*SpilledDataStorageSize、usedDisk、spillStorage.timeWaitingMicros记录了 14 种典型管道在极端内存限制下的溢出形态并通过 Golden Test 机制jstests/query_golden/logs_spilling_md.js与源码实现document_source_sort.cpp、bucket_auto_stage.cpp、graph_lookup_stage.cpp、spillable_deque.cpp 等相互印证。掌握了这套字段命名规律与触发参数你就拥有了在慢查询日志中快速识别聚合内存瓶颈的实战能力。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网