MongoDB 聚合管道 Spilling 统计实战:慢查询日志字段、内存上限参数与 SBE golden 测试验证

发布时间:2026/9/13 1:21:58
MongoDB 聚合管道 Spilling 统计实战:慢查询日志字段、内存上限参数与 SBE golden 测试验证 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变体对应强制使用 SBESlot Based Execution查询引擎运行的结果同一测试在其他引擎/计划排序模式下会有各自独立的期望输出文件。1.1 输出中的 X 占位约定注意原文档中形如sortSpilledBytes : X的字符串。这不是模糊描述而是测试代码 logs_spilling_md.js 中getSpillingAttrs()函数做的显式归一化以Spills或SpilledRecords结尾的键如sortSpills、sortSpilledRecords保留真实数值——它们是确定性的直接参与 golden 比对以SpilledBytes或SpilledDataStorageSize结尾的键被替换为X——其取值依赖文档二进制大小与存储布局跨平台不稳定spillStorage子对象中的各键如timeWaitingMicros同样替换为X其中data子键被整体过滤测试中留有 TODOSERVER-109672说明待bytesRead字段确定性地出现后取消过滤。因此golden 比对真正约束的是哪些统计键出现、每个阶段溢写次数与记录数、是否用了磁盘usedDisk。2. 测试运行机制从 setParameter 到慢查询日志2.1 前置条件与标签测试文件头部声明了三个 tagslogs_spilling_md.jsrequires_persistenceSpilling 目标是磁盘上的 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 querycommand: 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 大内存上限internalQueryMaxBlockingSortMemoryUsageBytes10003 条小文档{a:1..3}可放入 1KB不溢写2. Sort 空集合同上1000—无数据不溢写3. Sort 溢写同上1每条记录都需落盘4. 多次 Sort同上1两个$sort各自独立统计6. GroupinternalDocumentSourceGroupMaxMemoryBytes、internalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpill1hash agg 溢写7/8. TextOrinternalTextOrStageMaxMemoryBytes1仅在 feature flagExtendedAutoSpilling存在且开启时运行L1659. BucketAutointernalDocumentSourceBucketAutoMaxMemoryBytes1$bucketAuto 分桶溢写10. HashLookupinternalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpill1$lookup 哈希构建侧溢写11/12. GraphLookupinternalDocumentSourceGraphLookupMaxMemoryBytes1$graphLookup 游标侧溢写13. HashLookupUnwindinternalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill1$lookup$unwind 改写为 hash join 后溢写14. SetWindowFieldsinternalDocumentSourceSetWindowFieldsMaxMemoryBytes1SBE/ 500Classic见下方特殊处理其中internalQueryMaxBlockingSortMemoryUsageBytes的 IDL 定义L216-L226写明它限制一条查询执行阻塞式排序的内存字节即使允许使用磁盘该上限仍然约束内存消耗默认值为100 * 1024 * 1024100MB。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用例 1Sort with large memory limit—— 内存上限充足时不发生溢写Spilling 统计为空对象{ }[ { $sort : { a : 1 } } ]{ }用例 2Sort with empty collection—— 空集合排序同样无溢写统计[ { $sort : { a : 1 } } ]{ }用例 3Sort with spilling—— 1 字节上限下3 条记录的阻塞式排序发生 5 次溢写、8 条记录落盘记录数大于文档数是因为溢出文件合并阶段产生的中间记录并置位usedDisk[ { $sort : { a : 1 } } ]{ sortSpilledBytes : X, sortSpilledDataStorageSize : X, sortSpilledRecords : 8, sortSpills : 5, usedDisk : true }用例 4Multiple 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用例 6hash 聚合阶段与后续 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 阶段用例 7TextOr 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 }用例 8TextOr 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关联students8 条其中 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 条构成有向图的文档上执行$graphLookupstartWith: 1沿to-_id递归用例 11Graph 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 }用例 12Graph 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用例 14sbeFull变体下窗口阶段被下推到 SBEWINDOW阶段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_mapPlanSummaryStats::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.cppClassic 路径则由 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 \ --suitesquery_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、$lookuphash 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),仅供参考