MongoDB 慢查询日志中的聚合 Spilling 统计:logs_spilling 黄金测试与 SpillingStats 实现解析
2026/9/16 17:29:01 网站建设 项目流程

MongoDB 慢查询日志中的聚合 Spilling 统计:logs_spilling 黄金测试与 SpillingStats 实现解析

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

本文以jstests/query_golden/expected_output/sbeDisabled/logs_spilling.md这份黄金测试期望输出文件为主线,完整讲解 MongoDB 聚合管道中各类阶段($sort$group$bucketAuto$graphLookup$setWindowFields等)发生内存溢出(spilling)时,慢查询日志中各 spilling 统计字段的含义、期望取值,以及它们在 logs_spilling_md.js 测试脚本与 SpillingStats 源码中的对应实现。读完后你将能够:看懂慢查询日志中usedDisk*Spills*SpilledRecords*SpilledBytes等字段的语义,掌握通过setParameter强制各阶段溢出的验证手法,并理解同一测试在不同引擎变体(sbeDisabled/sbeFull等)下期望输出为何不同。

一、这份文档在仓库中的定位

expected_output/sbeDisabled/logs_spilling.md是黄金测试(golden test)框架下的期望输出文件,对应测试脚本 logs_spilling_md.js。该测试的声明目标是:"Tests the spilling statistics are a part of slow query logs"(验证 spilling 统计是慢查询日志的一部分),前置标签为:

  • requires_persistence:spilling 依赖持久化存储;
  • requires_fcv_81:需要 8.1 及以上的功能兼容版本;
  • requires_profiling:依赖 profiling 机制捕获慢查询日志。

目录名sbeDisabled表明这份期望文件适用于经典(Classic)查询引擎变体——即禁用 SBE(Slot-Based Execution)的 mongod 配置。同一测试脚本在其他引擎变体下会产生不同的期望输出(expected_output/下另有 sbeFull/ 等兄弟目录),因为 SBE 阶段(如 HashLookup、BlockHashAgg)与经典阶段的溢出行为、统计口径并不完全一致。

测试运行方式与整个query_golden套件一致(参见 README.plan_stability.md 中对黄金测试的通用说明,套件定义见 query-optimization/BUILD.bazel 中的query_golden_classic等 target):

buildscripts/resmoke.py run --suites=query_golden_classic \ jstests/query_golden/logs_spilling_md.js

测试失败时,buildscripts/golden_test.py diff会展示期望输出与实际日志输出的差异;确认行为变更后用buildscripts/golden_test.py accept接受新的期望文件。

二、期望文件的结构约定:14 个用例如何组织

整个.md文件由 14 个编号用例(## 1. ...## 14. ...)组成,每个用例包含:

  • ### Pipeline:触发该场景的聚合管道(EJSON);
  • ### Slow query spilling stats:从慢查询日志中按规则抽取的 spilling 统计;
  • 少数用例(11、12、14)还多出### Slow query spill storage stats一节,记录 spill 存储层的等待时间。

两个关键的解读规则:

  1. "X"是占位符,不是字面值。测试脚本中的getSpillingAttrs()函数会把取值不确定(依赖数据布局、分配器行为)的字节类字段统一替换为"X"再与期望文件比对:
    • SpilledBytesSpilledDataStorageSize结尾的字段 → 写成"X"(字符串);
    • spillStorage子文档中除data外的统计(当前只有timeWaitingMicros)→ 同样写成"X"
    • usedDisk、以Spills/SpilledRecords结尾的计数字段是确定性的,期望文件中保留精确数值(如"sortSpilledRecords" : 8)。
  2. 空对象{ }表示该管道没有产生任何 spilling 字段——即该阶段在这个引擎变体下要么没有溢出,要么根本不汇报 spilling 统计。这一点在用例 10/13(见第五节)中尤为重要。

三、完整用例与期望统计逐条解析

以下按原文件顺序完整收录 14 个用例,并补充每个用例在测试脚本中设置的强制溢出参数。

用例 1:大内存上限下的 $sort —— 不溢出

测试把internalQueryMaxBlockingSortMemoryUsageBytes设为 1000(字节),插入 3 条小文档后执行$sort,排序完全在内存中完成,因此日志中没有 spilling 字段:

[ { "$sort" : { "a" : 1 } } ]

期望:

{ }

用例 2:空集合上的 $sort —— 不溢出

空集合上执行同样的$sort,同样期望{ }。这两个用例验证了:没有实际发生 spilling 时,慢查询日志不应出现 spilling 统计,避免日志噪声。

用例 3:强制溢出的 $sort

internalQueryMaxBlockingSortMemoryUsageBytes压到1 字节,任何数据都放不下,3 条文档全部落盘:

[ { "$sort" : { "a" : 1 } } ]
{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 8, "sortSpills" : 5, "usedDisk" : true }

注意sortSpilledRecords为 8 而非 3:从源码结构看(document_source_sort.cpp 及其统计汇聚逻辑),排序溢出的 record 计数把内部产生的记录(含 key/文档两侧)一并计入,故与输入文档数不同;sortSpills: 5表示触发溢出的批次数。这两个数值在测试数据固定时是确定性的,所以能写入期望文件。

用例 4:管道中多个 $sort

插入 3 条{a, b}文档,执行两段排序:

[ { "$sort" : { "a" : 1 } }, { "$limit" : 3 }, { "$sort" : { "b" : 1 } } ]

期望:

{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 16, "sortSpills" : 10, "usedDisk" : true }

关键字义:同一管道内多个排序阶段的 spilling 统计会被累加到同一组字段(两段各贡献约 8 records、5 spills)。这与SpillingStats::accumulate()的逐字段累加实现(spilling_stats.h)一致,也与 plan_summary_stats_visitor.h 中visit(SortStats)把多个 Sort 节点的spillingStats累入spillingStatsPerStage[SORT]的逻辑对应。

用例 5:时间序列集合上的 $sort

测试先用initTimeseriesColl()创建时间序列集合并写入 50 条文档(间隔为bucketMaxSpanSeconds / 10),再执行{ "$sort" : { "time" : 1 } }。即使内存上限保持 1 字节,期望是:

{ "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 50, "sortSpills" : 50, "usedDisk" : true }

与用例 3 不同,这里是 50 条记录、50 次溢出——时间序列集合的读取路径产生了更细粒度的分批溢出,且 record 数与输入文档数一一对应。

用例 6:$group + $sort 同时溢出

测试同时把internalDocumentSourceGroupMaxMemoryBytesinternalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpill设为 1,插入 4 条两组的文档:

[ { "$group" : { "_id" : "$a", "b" : { "$sum" : "$b" } } }, { "$sort" : { "b" : 1 } } ]

期望中同时出现 group 与 sort 两组统计

{ "groupSpilledBytes" : "X", "groupSpilledDataStorageSize" : "X", "groupSpilledRecords" : 4, "groupSpills" : 4, "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 4, "sortSpills" : 3, "usedDisk" : true }

这验证了 spilling 统计是按阶段前缀区分的(group*/sort*),同一条慢查询日志可以同时携带多个阶段的溢出信息。

用例 7:TextOr 投影(ExtendedAutoSpilling 特性)

该用例(及其后的用例 8)位于FeatureFlagUtil.isPresentAndEnabled(db, "ExtendedAutoSpilling")保护之下,仅当ExtendedAutoSpilling特性存在且启用时执行;测试将internalTextOrStageMaxMemoryBytes设为 1,并在文本索引集合上执行:

[ { "$match" : { "$text" : { "$search" : "black tea" } } }, { "$addFields" : { "score" : { "$meta" : "textScore" } } } ]

期望:

{ "textOrSpilledBytes" : "X", "textOrSpilledDataStorageSize" : "X", "textOrSpilledRecords" : 4, "textOrSpills" : 4, "usedDisk" : true }

说明sbeDisabled变体下该特性处于启用状态,否则期望文件中不会有 7、8 两节。对应地,plan_summary_stats_visitor.h 中visit(TextOrStats)会把溢出累入SpillingStage::TEXT_OR并置usedDisk = true

用例 8:TextOr + 按 textScore 排序

[ { "$match" : { "$text" : { "$search" : "black tea" } } }, { "$sort" : { "_" : { "$meta" : "textScore" } } } ]

期望同时携带textOr*(4/4)与sort*(8 records、5 spills)两组统计且usedDisk: true,进一步印证多阶段统计在同一日志行中共存的规则。

用例 9:$bucketAuto

internalDocumentSourceBucketAutoMaxMemoryBytes设为 1,插入 4 条两组文档:

[ { "$bucketAuto" : { "groupBy" : "$a", "buckets" : 2, "output" : { "sum" : { "$sum" : "$b" } } } } ]

期望:

{ "bucketAutoSpilledBytes" : "X", "bucketAutoSpilledDataStorageSize" : "X", "bucketAutoSpilledRecords" : 13, "bucketAutoSpills" : 7, "usedDisk" : true }

bucketAutoSpilledRecords: 13大于输入 4 条,从源码结构看(document_source_bucket_auto.cpp),bucket 划分过程中的中间记录也被计入溢出记录数。

用例 10:$lookup(HashLookup)在经典引擎下不上报溢出

测试把internalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpill设为 1,准备 students/people 两个集合执行$lookup,但期望是空的

[ { "$lookup" : { "from" : "logs_spilling_md_students", "localField" : "name", "foreignField" : "name", "as" : "matched" } } ]
{ }

这正是sbeDisabled目录的意义所在:HashLookup 溢出统计属于 SBE 阶段(sbe_hash_lookup_shared_test.h、hash_lookup.cpp),经典引擎执行$lookup时走的是不产生这类 spilling 统计的路径,因此即便参数被压低,经典变体的期望输出仍是空对象。对比sbeFull等变体的期望文件,此处应能看到hashLookup*字段——这是同一脚本在不同引擎下产生不同期望输出的典型例子。

用例 11:$graphLookup 溢出(含 spill storage 统计)

internalDocumentSourceGraphLookupMaxMemoryBytes设为 1,插入 7 条{_id, to}图数据:

[ { "$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 }

并额外多出一节 spill 存储层统计:

### Slow query spill storage stats { "timeWaitingMicros" : "X" }

timeWaitingMicros记录执行期等待 spill 存储 I/O 的时间(微秒),因为依赖实际 I/O 时序所以取值为"X"。从 spillable_deque.cpp 等实现可见,spill 存储层(spillStorage)自身维护独立于阶段统计的等待时间指标。

用例 12:$graphLookup + $unwind + $sort

在图查询后追加解包与排序:

[ { "$limit" : 1 }, { "$graphLookup" : { "from" : "coll", "startWith" : 1, "connectFromField" : "to", "connectToField" : "_id", "as" : "path", "depthField" : "depth" } }, { "$unwind" : "$path" }, { "$sort" : { "path.depth" : 1 } } ]

期望的 spilling 统计与用例 11 相同(graphLookup*:2 records / 2 spills),而$unwind后的排序因数据量极小未触发溢出,故没有sort*字段。这说明日志中只出现真正溢出的阶段字段——usedDisk与各阶段字段的存在本身就是"该阶段发生溢出"的判据。

用例 13:$lookup + $unwind(HashLookupUnwind)

internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill设为 1,animals/locations 集合执行 lookup-unwind-project:

[ { "$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 } } ]

与用例 10 同理,经典引擎下期望为空{ }hashJoin*统计同样属于 SBE 路径,见 hash_join.cpp 与 plan_summary_stats_visitor.h 中visit(sbe::HashJoinStats)的累加逻辑)。

用例 14:$setWindowFields(两个变体)

测试先通过explain判断窗口阶段是否下推到 SBE(WINDOWstage),若是则把internalDocumentSourceSetWindowFieldsMaxMemoryBytes设为 1,否则设为 500(注释说明:经典引擎的DocumentSourceSetWindowFields在溢出后仍装不进内存上限时会失败,因此不能设 1 字节):

[ { "$setWindowFields" : { "partitionBy" : "$a", "sortBy" : { "b" : 1 }, "output" : { "sum" : { "$sum" : "$b" } } } } ]

期望(第一个变体,无$limit):

{ "setWindowFieldsSpilledBytes" : "X", "setWindowFieldsSpilledDataStorageSize" : "X", "setWindowFieldsSpilledRecords" : 3, "setWindowFieldsSpills" : 2, "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 13, "sortSpills" : 7, "usedDisk" : true }

外加 spill storage 统计{ "timeWaitingMicros" : "X" }。注意窗口阶段内部排序的溢出同样计入sort*字段(setWindowFields*sort*并存)。

第二个变体在管道尾部追加{ "$limit" : 1 },期望变为:

{ "setWindowFieldsSpilledBytes" : "X", "setWindowFieldsSpilledDataStorageSize" : "X", "setWindowFieldsSpilledRecords" : 2, "setWindowFieldsSpills" : 1, "sortSpilledBytes" : "X", "sortSpilledDataStorageSize" : "X", "sortSpilledRecords" : 13, "sortSpills" : 7, "usedDisk" : true }

$limit使窗口阶段提前结束,溢出记录数从 3 降到 2、溢出次数从 2 降到 1——下游 limit 会真实减少上游的溢出量,这是该用例的验证点。

四、测试脚本如何产生并比对这些日志

logs_spilling_md.js 的核心机制值得细读,它是"如何主动观测 spilling 统计"的可复制模板:

  1. 打开全量慢日志db.setProfilingLevel(1, {slowms: -1})——profiling 级别 1、阈值 -1 毫秒,任何查询都会记录"Slow query"日志;finally块中恢复原 profiling 配置,并逐一恢复所有改过的setParameter值(parametersToRestore列表保证不留脏状态)。
  2. 强制溢出:每个用例前用saveParameterToRestore(knob)记录原值,再把对应内存上限setParameter到 1 字节(或 500 字节)。涉及的参数全集为:
    • internalQueryMaxBlockingSortMemoryUsageBytes($sort)
    • internalDocumentSourceGroupMaxMemoryBytesinternalQuerySlotBasedExecutionHashAggApproxMemoryUseInBytesBeforeSpill($group)
    • internalTextOrStageMaxMemoryBytes(TextOr,受ExtendedAutoSpilling特性门控)
    • internalDocumentSourceBucketAutoMaxMemoryBytes($bucketAuto)
    • internalQuerySlotBasedExecutionHashLookupApproxMemoryUseInBytesBeforeSpill(SBE HashLookup)
    • internalDocumentSourceGraphLookupMaxMemoryBytes($graphLookup)
    • internalQuerySlotBasedExecutionHashJoinApproxMemoryUseInBytesBeforeSpill(SBE HashJoin)
    • internalDocumentSourceSetWindowFieldsMaxMemoryBytes($setWindowFields)
  3. 定位日志行:每次执行coll.aggregate(pipeline, {comment: comment})时携带唯一comment,随后db.adminCommand({getLog: "global"})拉取全局日志,用findMatchingLogLine()msg: "Slow query"+comment+command: "aggregate"三重过滤定位目标行——comment过滤确保不会误抓子操作(如索引构建)产生的日志。
  4. 抽取并脱敏getSpillingAttrs()遍历日志行的attr字段:键为usedDisk、以Spills/SpilledRecords结尾的保留原值;以SpilledBytes/SpilledDataStorageSize结尾的替换为"X"spillStorage子文档去掉data后其余键也替换为"X"(其中留有 TODO:待spillStorage.databytesRead字段确定性出现后停止过滤data键)。
  5. 格式化输出:通过pretty_md.jssection()/subSection()/code()输出## N. 标题+### Pipeline+### Slow query spilling stats的 Markdown 结构,交给黄金测试框架与期望文件逐字节比对。

五、源码级支撑:SpillingStats 四元组与 usedDisk 汇聚

期望文件里反复出现的四元组字段,其定义在 spilling_stats.h 的SpillingStats类中,四个私有成员语义明确:

字段源码注释语义对应日志字段
_spills"The number of times the tracked entity spilled."(溢出发生次数)*Spills
_spilledBytes随溢出释放的内存字节数*SpilledBytes("X")
_spilledDataStorageSize溢出占用的磁盘空间字节数*SpilledDataStorageSize("X")
_spilledRecords写入 record store 的记录数*SpilledRecords

两个实现细节解释了期望文件里的数值规律:

  • updateSpilledDataStorageSize()std::max取历史最大值并返回增量(spilling_stats.h),说明该指标是"高水位"而非累加值,字节数天然依赖运行细节,故测试一律以"X"脱敏。
  • accumulate()支持把多个执行节点的统计合并,这正是用例 4(多 $sort)、用例 6/8/14(多阶段)中同前缀字段数值相加的来源。

统计如何进入慢查询日志?plan_summary_stats_visitor.h 以树遍历方式访问执行计划各阶段的统计节点,对 SORT、GROUP、TEXT_OR、GEO_NEAR、SET_WINDOW_FIELDS、HASH_LOOKUP、HASH_JOIN、GRAPH_LOOKUP 等阶段统一执行同一模式:

if (stats->spillingStats.getSpills() > 0) { _summary.usedDisk = true; _summary.spillingStatsPerStage[PlanSummaryStats::SpillingStage::XXX].accumulate(...); }

由此得出两条可验证结论:

  1. usedDisk: true当且仅当至少一个阶段spills > 0——这就是用例 1/2/10/13 期望对象里连usedDisk都没有的原因;
  2. 各阶段统计以阶段为键分桶累加,最终序列化为sort*group*textOr*bucketAuto*graphLookup*setWindowFields*等前缀字段进入慢查询日志的attr部分。

六、实战要点小结

  • 在运维侧:看到慢查询日志中出现usedDisksortSpilledRecords等字段,即代表聚合管道发生了内存到磁盘的溢出,可按阶段前缀定位是哪个阶段;spillStorage.timeWaitingMicros则量化了等待 spill I/O 的开销。
  • 在测试侧:复现/验证某一阶段的溢出行为,只需把对应internal*MaxMemoryBytes/*ApproxMemoryUseInBytesBeforeSpill参数压到极小,再配合setProfilingLevel(1, {slowms: -1})comment标记抓取日志;期望文件中的确定性计数字段(*Spills*SpilledRecordsusedDisk)可用于断言,字节类字段则应视为非确定性。
  • 在引擎差异侧sbeDisabled变体中 $lookup/$lookup-unwind 用例期望为空,而 SBE 变体下同一段管道会汇报hashLookup*/hashJoin*统计——修改溢出逻辑或日志字段时,必须同步检查 expected_output/ 下所有引擎变体的期望文件并用buildscripts/golden_test.py accept更新,否则黄金测试将失败。

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询