MongoDB 聚合管道 Spilling 统计实战:慢查询日志字段、内存上限参数与 SBE golden 测试验证
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文围绕 MongoDB 仓库中一个 golden data 测试的期望输出文件 logs_spilling.md 展开:它固化了 14 类聚合管道在强制触发 Spilling(溢写)后,慢查询日志中应出现的 Spilling 统计字段与确定数值。读完本文,你将理解每个*Spills/*SpilledRecords/usedDisk字段的含义、控制各阶段溢写行为的internal*服务器参数、测试如何基于 profiling 日志提取并归一化这些统计,以及 golden 测试框架如何对该输出做 diff 与接受。
1. 这个文件在测试体系中的位置
logs_spilling.md 是 golden data 测试 logs_spilling_md.js 的"期望输出"(expected output)。MongoDB 的 golden data 测试框架将测试运行产生的文本输出与仓库中签入的已知正确输出逐字节比对,任何差异都会导致测试失败——框架设计与使用方式详见 golden_data_test_framework.md。该框架强调:输出必须确定、可 diff、增量变化,且期望输出文件不应手工修改,而是通过buildscripts/golden_test.py的diff/accept命令重新生成。
本测试验证的核心命题是:当聚合管道中的某个内存受限阶段(sort、group、hash lookup、$graphLookup、$bucketAuto、$setWindowFields 等)发生 Spilling 时,慢查询(Slow query)日志行中必须携带该阶段的 Spilling 统计属性。测试通过把各阶段内存上限压到 1 字节附近,强制每个阶段落盘,然后断言日志里出现了预期的统计键与确定数值。
文件位于expected_output/sbeFull/目录下。从目录命名与 golden 框架文档中internalQueryFrameworkControl的说明可以推断:sbeFull变体对应强制使用 SBE(Slot Based Execution)查询引擎运行的结果,同一测试在其他引擎/计划排序模式下会有各自独立的期望输出文件。
1.1 输出中的 "X" 占位约定
注意原文档中形如"sortSpilledBytes" : "X"的字符串。这不是模糊描述,而是测试代码 logs_spilling_md.js 中getSpillingAttrs()函数做的显式归一化:
- 以
Spills或SpilledRecords结尾的键(如sortSpills、sortSpilledRecords)保留真实数值——它们是确定性的,直接参与 golden 比对; - 以
SpilledBytes或SpilledDataStorageSize结尾的键被替换为"X"——其取值依赖文档二进制大小与存储布局,跨平台不稳定; spillStorage子对象中的各键(如timeWaitingMicros)同样替换为"X";其中data子键被整体过滤,测试中留有 TODO(SERVER-109672)说明待bytesRead字段确定性地出现后取消过滤。
因此,golden 比对真正约束的是:哪些统计键出现、每个阶段溢写次数与记录数、是否用了磁盘(usedDisk)。
2. 测试运行机制:从 setParameter 到慢查询日志
2.1 前置条件与标签
测试文件头部声明了三个 tags(logs_spilling_md.js):
requires_persistence:Spilling 目标是磁盘上的 Record Store,必须持久化环境可用;requires_fcv_81:期望行为以 8.1 功能兼容性版本为准;requires_profiling:整个测试依赖 profiling 日志。测试首行即执行db.setProfilingLevel(1, {slowms: -1})(L90),使所有命令都进入慢查询日志,从而保证每个aggregate都能被观察到;finally块中恢复原有 profiling 级别与所有被修改的服务器参数。
2.2 统计提取流程
每个用例的执行骨架(L47-L72):
- 以固定
comment执行coll.aggregate(pipeline, {comment}); db.adminCommand({getLog: "global"})取全局日志;- 用 log.js 的
findMatchingLogLine匹配msg: "Slow query"+command: "aggregate"+ 指定comment的日志行(comment 用于避免误抓子操作日志); - 解析该 JSON 日志行的
attr字段,按第 1.1 节规则抽取 Spilling 统计与 spill storage 统计; - 以 Markdown 小节形式写出
Pipeline、Slow query spilling stats,以及(如有)Slow query spill storage stats。
2.3 各用例对应的内存上限参数
测试用setParameter把各阶段内存上限压到 1 字节附近触发溢写,并在finally中全部还原。参数定义见 stage_memory_limit_knobs.idl,均为运行时可设(set_at: [startup, runtime]):
| 用例(章节) | 服务器参数 | 测试取值 | 说明 |
|---|---|---|---|
| 1. Sort 大内存上限 | internalQueryMaxBlockingSortMemoryUsageBytes | 1000 | 3 条小文档({a:1..3})可放入 1KB,不溢写 |
| 2. Sort 空集合 | 同上(1000) | — | 无数据,不溢写 |
| 3. Sort 溢写 | 同上 | 1 | 每条记录都需落盘 |
| 4. 多次 Sort | 同上 | 1 | 两个$sort各自独立统计 |
| 6. Group | internalDocumentSourceGroupMaxMemoryBytes、internalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpill | 1 | hash agg 溢写 |
| 7/8. TextOr | internalTextOrStageMaxMemoryBytes | 1 | 仅在 feature flagExtendedAutoSpilling存在且开启时运行(L165) |
| 9. BucketAuto | internalDocumentSourceBucketAutoMaxMemoryBytes | 1 | $bucketAuto 分桶溢写 |
| 10. HashLookup | internalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpill | 1 | $lookup 哈希构建侧溢写 |
| 11/12. GraphLookup | internalDocumentSourceGraphLookupMaxMemoryBytes | 1 | $graphLookup 游标侧溢写 |
| 13. HashLookupUnwind | internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill | 1 | $lookup+$unwind 改写为 hash join 后溢写 |
| 14. SetWindowFields | internalDocumentSourceSetWindowFieldsMaxMemoryBytes | 1(SBE)/ 500(Classic) | 见下方特殊处理 |
其中internalQueryMaxBlockingSortMemoryUsageBytes的 IDL 定义(L216-L226)写明:它限制一条查询执行阻塞式排序的内存(字节),即使允许使用磁盘,该上限仍然约束内存消耗;默认值为100 * 1024 * 1024(100MB)。
SetWindowFields 的特殊处理(L396-L411):测试先对管道执行explain,若获胜计划中出现WINDOW阶段(即$setWindowFields被下推到 SBE),内存上限设为 1 字节;否则(Classic 引擎路径)设为 500 字节——因为 Classic 的DocumentSourceSetWindowFields在溢写后若仍装不下会直接报错,1 字节无法容忍。这解释了为何该 golden 文件放在sbeFull变体下却仍需引擎感知。
Timeseries 用例(L74-L88):创建timeField: "time"的时序集合,插入 50 条按bucketMaxSpanSeconds/10递增时间戳的文档,再执行$sort: {time: 1}。时序集合的时间顺序扫描使 sort 走有界(bounded)路径,但仍会记录 Spilling 统计。
3. 期望输出全文解读(14 个用例)
以下为 logs_spilling.md 中 14 个用例的完整期望内容,按主题分组讲解。
3.1 Sort 系列(用例 1–4)
用例 1:Sort with large memory limit—— 内存上限充足时不发生溢写,Spilling 统计为空对象{ }:
[ { "$sort" : { "a" : 1 } } ]{ }用例 2:Sort with empty collection—— 空集合排序同样无溢写统计:
[ { "$sort" : { "a" : 1 } } ]{ }用例 3:Sort with spilling—— 1 字节上限下,3 条记录的阻塞式排序发生 5 次溢写、8 条记录落盘(记录数大于文档数是因为溢出文件合并阶段产生的中间记录),并置位usedDisk:
[ { "$sort" : { "a" : 1 } } ]{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 8, "sortSpills" : 5, "usedDisk" : true }用例 4:Multiple sorts—— 管道内两个$sort(中间夹$limit: 3),统计是各 sort 阶段累积的结果:16 条记录、10 次溢写。这说明慢查询日志中的 Spilling 统计覆盖整条管道的同类阶段总量,而非单阶段:
[ { "$sort" : { "a" : 1 } }, { "$limit" : 3 }, { "$sort" : { "b" : 1 } } ]{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 16, "sortSpills" : 10, "usedDisk" : true }3.2 Group + Sort(用例 6)
hash 聚合阶段与后续 sort 阶段分别贡献一组统计键(group*与sort*),这是多阶段 Spilling 各自独立计数的直接证据:
[ { "$group" : { "_id" : "$a", "b" : { "$sum" : "$b" } } }, { "$sort" : { "b" : 1 } } ]{ "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" }从输出结构看,走 Record Store / spill storage 的聚合阶段(hash agg、hash join、window 等)还会上报spillStorage子统计(等待 Record Store 的时间等),而纯 sort 溢写路径不上报该子对象——用例 3、4 即无此小节。
3.3 TextOr(用例 7–8)
这两个用例仅在ExtendedAutoSpillingfeature flag 开启时存在,覆盖文本评分查询(textScore)相关的 TextOr 阶段:
用例 7:TextOr and projection($match+$text后$addFields取textScore):
[ { "$match" : { "$text" : { "$search" : "black tea" } } }, { "$addFields" : { "score" : { "$meta" : "textScore" } } } ]{ "textOrSpilledBytes" : "X", "textOrSpilledDataStorageSize" : "X", "textOrSpilledRecords" : 4, "textOrSpills" : 4, "usedDisk" : true }用例 8:TextOr and sort($sort按textScore排序)—— TextOr 与 sort 两组统计并存:
[ { "$match" : { "$text" : { "$search" : "black tea" } } }, { "$sort" : { "_" : { "$meta" : "textScore" } } } ]{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 8, "sortSpills" : 5, "textOrSpilledBytes" : "X", "textOrSpilledDataStorageSize" : "X", "textOrSpilledRecords" : 4, "textOrSpills" : 4, "usedDisk" : true }3.4 BucketAuto(用例 9)
$bucketAuto将输入自动分桶,分桶过程受internalDocumentSourceBucketAutoMaxMemoryBytes约束:
[ { "$bucketAuto" : { "groupBy" : "$a", "buckets" : 2, "output" : { "sum" : { "$sum" : "$b" } } } } ]{ "bucketAutoSpilledBytes" : "X", "bucketAutoSpilledDataStorageSize" : "X", "bucketAutoSpilledRecords" : 13, "bucketAutoSpills" : 7, "usedDisk" : true }3.5 HashLookup(用例 10)
对people集合(4 条:Alex、Drew、Justin、Parker)执行$lookup关联students(8 条,其中 Alex、Harley 各有重名),构建侧哈希表被限制到 1 字节内存:
[ { "$lookup" : { "from" : "logs_spilling_md_students", "localField" : "name", "foreignField" : "name", "as" : "matched" } } ]{ "hashLookupSpilledBytes" : "X", "hashLookupSpilledDataStorageSize" : "X", "hashLookupSpilledRecords" : 14, "hashLookupSpills" : 16, "usedDisk" : true }{ "timeWaitingMicros" : "X" }注意from中的集合名被渲染为测试名拼接的logs_spilling_md_students(测试以db[jsTestName() + "_students"]建集合,L228-L230),这正是 golden 测试"输出必须跨运行确定"要求的体现——集合名必须可预测。
3.6 GraphLookup(用例 11–12)
在 7 条构成有向图的文档上执行$graphLookup(startWith: 1,沿to->_id递归):
用例 11:Graph lookup:
[ { "$limit" : 1 }, { "$graphLookup" : { "from" : "coll", "startWith" : 1, "connectFromField" : "to", "connectToField" : "_id", "as" : "path", "depthField" : "depth" } } ]{ "graphLookupSpilledBytes" : "X", "graphLookupSpilledDataStorageSize" : "X", "graphLookupSpilledRecords" : 2, "graphLookupSpills" : 2, "usedDisk" : true }用例 12:Graph lookup with unwind and sort—— 追加$unwind: "$path"与$sort: {"path.depth": 1}后,期望统计与用例 11 完全一致(2 记录 / 2 次溢写),说明该场景下后续 unwind/sort 未产生额外 Spilling 统计。两个用例均带 spill storage 小节:
{ "timeWaitingMicros" : "X" }3.7 HashLookupUnwind(用例 13)
$lookup + $unwind会被优化为 hash join(受internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill控制)。测试还额外创建了两个 dummy 索引({"dummy": -1, "locationName": -1}与{"dummy": 1, "name": -1}),注释说明这是为 join 优化提供 multikey 信息的(L350-L352):
[ { "$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 } } ]{ "hashLookupSpilledBytes" : "X", "hashLookupSpilledDataStorageSize" : "X", "hashLookupSpilledRecords" : 6, "hashLookupSpills" : 6, "usedDisk" : true }{ "timeWaitingMicros" : "X" }值得注意:hash join 路径复用了hashLookup*前缀的统计键。
3.8 SetWindowFields(用例 14)
sbeFull变体下窗口阶段被下推到 SBE(WINDOW阶段),1 字节上限即可触发溢写。无$limit变体:
[ { "$setWindowFields" : { "partitionBy" : "$a", "sortBy" : { "b" : 1 }, "output" : { "sum" : { "$sum" : "$b" } } } } ]{ "setWindowFieldsSpilledBytes" : "X", "setWindowFieldsSpilledDataStorageSize" : "X", "setWindowFieldsSpilledRecords" : 4, "setWindowFieldsSpills" : 4, "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 13, "sortSpills" : 7, "usedDisk" : true }追加$limit: 1变体——窗口阶段自身统计从 4/4 降为 3/3,而sort*统计不变,表明$limit削减了窗口处理的输入量,但内部排序的溢写总量不受尾部截断影响:
[ { "$setWindowFields" : { "partitionBy" : "$a", "sortBy" : { "b" : 1 }, "output" : { "sum" : { "$sum" : "$b" } } } }, { "$limit" : 1 } ]{ "setWindowFieldsSpilledBytes" : "X", "setWindowFieldsSpilledDataStorageSize" : "X", "setWindowFieldsSpilledRecords" : 3, "setWindowFieldsSpills" : 3, "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 13, "sortSpills" : 7, "usedDisk" : true }两个变体同样附带{ "timeWaitingMicros" : "X" }的 spill storage 小节。
4. 源码纵深:统计从哪里来
4.1 SpillingStats 四类计数器
所有 Spilling 统计都由 spilling_stats.h 中的SpillingStats类累积,共四个计数器(L73-L82):
_spills:发生溢写的次数(对应日志*Spills);_spilledBytes:因溢写而释放的内存字节数(对应*SpilledBytes,被测试归一化为 "X");_spilledDataStorageSize:溢写占用的磁盘空间字节数(对应*SpilledDataStorageSize,取累计最大值而非求和,见updateSpilledDataStorageSize的std::max语义);_spilledRecords:写入 Record Store 的记录条数(对应*SpilledRecords)。
这也解释了测试归一化策略的合理性:次数与记录数由数据量决定、可复现;字节数与磁盘占用受文档序列化尺寸影响、跨平台不稳定。
4.2 慢查询日志中的承载结构
op_debug.h 中PlanSummaryStats持有按阶段分组的统计映射absl::flat_hash_map<PlanSummaryStats::SpillingStage, SpillingStats> spillingStatsPerStage,即每条慢查询日志的attr里每个*Spills/*SpilledRecords/*SpilledBytes/*SpilledDataStorageSize键都对应一种SpillingStage枚举值(sort、group、bucketAuto、hashLookup、graphLookup、setWindowFields、textOr 等前缀即由此展开)。usedDisk则是PlanSummaryStats的独立布尔字段(L190),表示该查询计划整体使用了磁盘。
从源码结构看,SBE 侧各阶段实现各自向该体系写入统计:排序(sort.cpp、sort_stage_sort_impl.cpp)、哈希聚合(hash_agg.cpp、block_hashagg.cpp)、哈希查找(hash_lookup.cpp)、哈希连接(hash_join.cpp、hash_lookup_unwind.cpp)、窗口函数(window.cpp);Classic 路径则由 sort_stage.cpp、group_base_stage.cpp、graph_lookup_stage.cpp、internal_set_window_fields_stage.cpp 等实现同样的上报。golden 文件放在sbeFull变体下,比对的就是 SBE 路径产出的键集合与数值。
4.3 内存上限参数的定义位置
测试调用的九个internal*内存旋钮统一定义在 stage_memory_limit_knobs.idl(如internalDocumentSourceGroupMaxMemoryBytes位于 L51 附近,internalQueryMaxBlockingSortMemoryUsageBytes位于 L216-L226)。以阻塞式 sort 为例,其 IDL 描述明确:"限制一条查询执行阻塞式排序愿意使用的内存(字节);若允许磁盘使用,可能排序更多数据,但该限制仍约束内存消耗",默认 100MB。生产环境中该参数用于给单条查询的排序内存划红线;测试则把它压到 1 字节来穷举溢写路径。
5. 实操:运行、比对与接受新输出
5.1 运行该 golden 测试
该测试属于query_golden套件,标准运行方式参照同套件 README.plan_stability.md 中给出的 resmoke 用法,将引擎控制参数指向 SBE 以匹配sbeFull期望输出:
buildscripts/resmoke.py run \ --suites=query_golden_classic \ '--mongodSetParameters={internalQueryFrameworkControl: forceSBEEngine}' \ jstests/query_golden/logs_spilling_md.js其中internalQueryFrameworkControl正是 golden 框架文档中说明的、决定查询框架(Classic 或 SBE)的开关,不同取值对应expected_output/下不同子目录的期望文件。注意运行环境必须满足测试标签:持久化存储(Spilling 落盘)与 profiling 支持;requires_fcv_81意味着期望输出以 8.1 FCV 行为为准。
5.2 diff 与 accept 工作流
按 golden_data_test_framework.md 的流程,一次性完成工作站配置(buildscripts/golden_test.py setup,设置GOLDEN_TEST_CONFIG_PATH)后:
# 查看本次运行与期望输出的差异 buildscripts/golden_test.py diff # 将实际输出批量接受为新的期望输出(不要手工编辑期望文件) buildscripts/golden_test.py accept若同一测试需要在多种构建变体/引擎下同步刷新期望文件,使用框架提供的批量命令:
buildscripts/golden_test.py --verbose clean-run-accept jstests/query_golden/logs_spilling_md.js该命令通过resmoke.py find-suites判定测试所属套件,并在必要时以不同的internalQueryFrameworkControl取值多轮运行,保证各变体的期望输出一致更新。
5.3 失败时如何定位
- 若 diff 中出现新增/缺失的统计键(如某阶段突然出现
*Spills),通常是该阶段引入了新的 Spilling 路径或统计上报,需要对照 4.2 节中对应的阶段实现文件确认上报逻辑; - 若
*SpilledRecords/*Spills的数值变化(如用例 14 的 4/4 变为 3/3),说明溢写合并策略或 Record Store 批处理行为改变了,应检查相应 SBE 阶段(sort/hash_agg/window 等)的溢写实现; usedDisk从true变false则意味着溢写未再发生,往往与内存上限参数或阶段改写(例如 join 优化是否生效)相关。
6. 小结
logs_spilling.md 这份看似简单的期望输出文件,实际上固化了 MongoDB 慢查询日志中 Spilling 可观测性的完整契约:14 个用例覆盖阻塞式排序、时序集合排序、hash 聚合、TextOr、$bucketAuto、$lookup(hash lookup 与 hash join 两条路径)、$graphLookup、$setWindowFields共 8 类内存受限阶段;每个用例断言了确定性的溢写次数与记录数、usedDisk标志,以及(对走 Record Store 的阶段)spillStorage.timeWaitingMicros的存在性。配合 logs_spilling_md.js 中"压低内存上限 → 强制溢写 → 从 profiling 日志抽取 → 归一化字节量 → golden 比对"的闭环,以及 spilling_stats.h、op_debug.h 与 stage_memory_limit_knobs.idl 中的源码实现,它既是一份运维上解读*Spills/*SpilledRecords/usedDisk日志字段的权威参照,也是一套防止 Spilling 统计回归的工程防线。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考