ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MongoDB 分片集群下 $group 下推(Pushdown)与分片键定位:基于 query_golden_sharding 黄金测试的深度剖析

MongoDB 分片集群下 $group 下推(Pushdown)与分片键定位:基于 query_golden_sharding 黄金测试的深度剖析 MongoDB 分片集群下 $group 下推Pushdown与分片键定位基于 query_golden_sharding 黄金测试的深度剖析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文以 MongoDB 源码仓库中的黄金测试Golden Test文档jstests/query_golden_sharding/expected_output/sbeRestricted/group_targeting.md为核心结合其驱动测试jstests/query_golden_sharding/group_targeting.js与底层执行框架系统讲解分片集群中聚合管道的$group何时能够被完全下推到各分片shard本地执行、何时只能部分下推或完全不下推以及下推后如何借助 DISTINCT_SCAN、SBE 的$groupByDistinctScan等机制获得最优执行计划。读完本文你将能准确预判$group在分片集合上的下推行为理解 explain 输出中$willBeMerged、$doingMerge、mergeType等关键字段的含义并掌握撰写与阅读此类黄金测试文档的方法。一、背景什么是 Golden Test 与group_targeting测试1.1 黄金测试Golden Test机制在 MongoDB 源码仓库中jstests/query_golden_sharding/目录存放着一组专门针对分片集合查询/聚合执行计划的黄金测试。所谓黄金测试是指测试脚本运行后将其输出包括查询 Pipeline、结果集、集合上的全部索引、以及经过规整的 explain 计划与仓库中预先固化下来的 Markdown 文件逐字比对。这些 Markdown 文件即为黄金输出任何计划形态或结果顺序的变动都会导致测试失败从而起到执行计划回归锁的作用。测试脚本jstests/query_golden_sharding/group_targeting.js黄金输出SBE 受限模式jstests/query_golden_sharding/expected_output/sbeRestricted/group_targeting.md同一测试在不同执行引擎/特性组合下还有多份黄金输出例如sbeDisabled/、sbeFull/、featureFlagSbeFull/、featureFlagSbeAccumulatorExpressions/等目录中的同名文件用于锁定不同配置下的计划差异。1.2 测试数据的构造驱动测试脚本通过ShardingTest({shards: 2})启动一个两分片的分片集群jstests/libs/shardingtest.js然后在test.group_targeting集合上执行如下操作创建单字段分片键shardKey的索引与分片集合在分片键shard1处执行split并将包含shard1_1的块通过moveChunk迁移到第二个分片在分片键上插入 11 条精心构造的文档数据覆盖了大小写shard0_*/sHaRd0_2/shARD1_3等边界情况。测试脚本在注释中特别指出文档{_id: 6.5, shardKey: shARD1_3}因为字符串shARD1_3 shard1_1按二进制排序实际上仍然位于分片 0。这一点在验证结果时必须小心也解释了为何最终分组结果中会同时出现shARD1_3与shard1_1两个不同组。之后测试调用outputAggregationPlanAndResults定义于 jstests/libs/query/golden_test_utils.js该辅助函数依次执行聚合、抓取 explain、规范化输出最终落盘为本文所述的 Markdown 黄金文件。每个用例的输出固定包含四块内容Pipeline、Results、Total indexes on the collection和Summarized explain。二、完全下推_id等于分片键时的最优执行计划2.1 最简形态$group的_id即分片键第一个用例的 Pipeline 只有一行[ { $group : { _id : $shardKey } } ]这是一个按分片键去重分组的最简场景。由于分组键就是分片键同组文档必然位于同一分片因此路由层mongos无需在合并端再做第二次分组$group可以被完全下推到各分片执行。观察黄金输出中的 Summarized explain可以提炼出完整下推的三条核心证据shardsPart下发给分片执行的片段中$group带有$willBeMerged : false明确告知这个组在分片端已经收敛完成不需要再参与合并shardsPart : [ { $group : { $willBeMerged : false, _id : $shardKey } } ]mergerPartmongos 端合并片段只有$mergeCursors没有任何$doingMerge分组节点说明路由端只是简单合并各分片游标。mergeType为router与上述两条相互印证。2.2 DISTINCT_SCAN由索引支撑的去重扫描在完全下推的基础上黄金输出还展示了分片端计划器的最优选择——DISTINCT_SCANwinningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, shardKey : 1 } }, { direction : forward, indexBounds : { shardKey : [ [MinKey, MaxKey] ] }, indexName : shardKey_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { shardKey : 1 }, multiKeyPaths : { shardKey : [ ] }, stage : DISTINCT_SCAN } ]可以看到计划器选择了集合上唯一的shardKey_1索引集合总索引为[ _id_, shardKey_1 ]见 Total indexes on the collection 一节以DISTINCT_SCAN直接跳过重复键值并且由于只投影shardKey字段、无需回表读取文档isFetching: false整体形成PROJECTION_COVERED的覆盖查询。随后在$cursor之上叠加了一个特殊的$groupByDistinctScan节点直接把去重扫描的每一行转换为{ _id : $shardKey }形态的分组结果{ $groupByDistinctScan : { newRoot : { _id : $shardKey } } }换句话说$group { _id: $shardKey }这种去重型分组在 SBESlot-Based Execution Engine槽位执行引擎下被识别为可等价转化为 distinct 语义从而获得比哈希分组更高的执行效率。这也是该黄金测试以sbeRestrictedSBE 受限特性组合为默认输出目录、并同时维护多份特性开关变体的原因——$groupByDistinctScan是否出现取决于相关特性标志的组合。2.3 从 explain 反推数据分布分片键排序陷阱需要提醒的是测试数据故意构造了大小写混合的分片键值。从结果集可以看出分组键按字符串二进制顺序排列sHaRd0_2和shARD1_3分别排在shard0_1与shard1_1之前。这正好呼应测试脚本中shARD1_3落在分片 0 的注释——分片键的排序比较是二进制字节序不是字典序locale序在阅读黄金输出、特别是带 collation 的用例时尤其要留意这一点。三、完全下推的边界_id为分片键的超集与简单重命名3.1_id是分片键的超集superset当分组键是分片键的超集例如_id: [$shardKey, $_id]由于分组键包含了分片键同组文档依然必然位于同一分片因此仍然可以完全下推。黄金输出中$willBeMerged: false、mergerPart仅含$mergeCursors、mergeType: router三者依旧成立shardsPart : [ { $group : { $willBeMerged : false, _id : [ $shardKey, $_id ] } } ]底层执行计划则退化为GROUP SHARDING_FILTER COLLSCAN分片端扫描全集合后按分组键聚合因为此时分组键不再是分片键本身无法直接借助 distinct 语义走DISTINCT_SCAN。3.2 简单重命名simple rename也允许完全下推测试还验证了在$group之前先做一次简单重命名投影的场景[ { $project : { renamedShardKey : $shardKey } }, { $group : { _id : $renamedShardKey } } ]$project只是把shardKey字段改名为renamedShardKey没有改变其值与语义。优化器能够穿透这一层投影识别出分组键依然对应原始分片键因此结果与直接按$shardKey分组完全一致且依旧完全下推。更进一步测试还验证了简单重命名的超集组合$project重命名 复合分组键{secretlyShardKey, actualId}同样是完全下推。核心判据只要优化器能够证明分组键的取值完全由分片键决定分片键是分组键的组成部分或分组键只是分片键的简单别名$group就可以完全下推反之一旦分组键是从分片键派生出来的新值情况就不同了。四、部分下推分组键由分片键派生时的折中策略4.1 派生键导致的分片内分组不一致当_id是分片键经表达式计算得到的派生值例如[ { $group : { _id : { $min : [ $shardKey, 1 ] } } } ]此时两个不同分片上的文档可能被映射到同一个派生分组键本例中所有以非1开头的分片键其$min结果都可能是同一个值。于是分片端的分组结果不再天然隔离mongos 必须再做一次合并分组。黄金输出中分片端$group不再带有$willBeMerged: falsemergerPart中出现了带$doingMerge: true的$group{ $group : { $doingMerge : true, _id : $$ROOT._id } }mergeType仍然为router。这就是部分下推partial pushdown分组计算在分片端并行执行分摊 CPU 与数据扫描但最终的分组收敛仍由路由端完成。测试中的更复杂用例派生键 $top累积器 后续$match/$sort展示了同样的模式$top在分片端与合并端各算一次合并端通过$$ROOT.otherField再次求$top。4.2 依赖其他字段的派生键派生键若还依赖非分片键字段如_id: {$min: [$shardKey, $_id]}结果集进一步分散每个文档的_id几乎都不同但计划形态依然是分片端 GROUP 路由端$doingMerge的 GROUP说明只要分组键不是分片键的保序别名就一律部分下推与派生键具体依赖哪些字段无关。4.3 多个$group时的下推判定测试还覆盖了管道中出现多个$group的场景验证了下推判定是逐阶段进行的管道下推结果第一个$group在分片键上{key, other}第二个$group按$_id.other仅第一个$group完全下推$willBeMerged: false第二个$group在路由端合并后执行第一个$group在分片键上第二个$group按$_id即原样聚合两个$group都被标记$willBeMerged: false双双下推第一个$group完全下推并计算$avg第二个$group按$avg计数第一个完全下推第二个部分下推路由端$doingMerge中执行$sum完成计数特别地Fully pushed down $group, followed by partially pushed down group 用例展示了累积器在两端的配合分片端计算avg路由端对avg再分组并用$sum: 1统计num最终正确得到每个平均值出现的次数。这说明下推优化可以精细到单个$group节点而不是全管道要么都下推、要么都不下推。五、不下推的典型场景保住正确性底线5.1 分组键不是分片键或只是改名伪装[ { $group : { _id : $otherField } } ]otherField与分片键无关同组文档可能分散在多个分片必须由路由端做完整合并分组。计划中分片端$group没有$willBeMerged标记mergerPart中$doingMerge: true的$group承担最终收敛。值得警惕的是下面这个改名伪装用例[ { $project : { shardKey : $otherField } }, { $group : { _id : $shardKey } } ]这里$project把非分片键otherField的值赋给了名为shardKey的字段。尽管分组键的字段名恰好叫shardKey但优化器穿透投影后发现其取值来自otherField与真实分片键毫无关系因此拒绝完全下推最终结果与直接按otherField分组相同。5.2 分片键没有被保留或后来才被添加$project: { otherField: 1 }后按$shardKey分组分片键字段已被投影丢弃管道中$shardKey解析为缺失值结果只有一行{ _id : null }自然无从下推。$project: { shardKey: 0 }$addFields: { shardKey: $otherField }后按$shardKey分组真实分片键先被删除、又被otherField覆盖同样拒绝下推。$addFields: { shardKey: $otherField }直接覆盖分片键字段后分组下推被拒绝分片端计划中出现PROJECTION_SIMPLE保留otherField与PROJECTION_DEFAULT计算新shardKey两级投影随后才是GROUP。5.3$$ROOT作为分组键[ { $group : { _id : $$ROOT } } ]$$ROOT表示整篇文档包含了_id、shardKey、otherField等全部字段本质上与分片键无关整篇文档集合不可能被分片键唯一决定故拒绝完全下推。而与之形成鲜明对比的[ { $group : { _id : $$ROOT.shardKey } } ]由于$$ROOT.shardKey就是分片键本身完全下推成立并且再次获得了DISTINCT_SCAN $groupByDistinctScan的最优计划。5.4 非简单 collation 阻断下推[ { $group : { _id : $shardKey } }, { $addFields : { _id : { $toLower : $_id } } } ]Options{ collation : { locale : en_US, strength : 2 } }这是一个非常微妙的正确性用例。分组键虽然是分片键但聚合指定了非简单 collationlocale: en_US, strength: 2大小写不敏感。在这样的排序规则下shARD1_3与shard1_3会被视为同一个分组键——而这两个值在分片键的二进制排序下却属于不同的值域、甚至位于不同的分片。若贸然下推各分片独立分组会得出比实际更多的组合并后必然产生重复_id。因此优化器拒绝完全下推改由路由端合并分组后再统一执行$toLower从而保证结果中_id全部小写由于shARD1_3与shard1_3不会同时存在测试数据只插入了shARD1_3最终没有出现重复_id。测试脚本在此用例前特意注释Note: If we have duplicate _ids in the output, that signals a bug here.若输出出现重复_id即表示此处存在 bug——这正是黄金测试的意义所在把不该下推却下推的回归风险显式地暴露出来。六、复合分片键compound shard key下的下推规则测试的第二大部分section(Sharded collection with compound key)在复合分片键{sk0: 1, sk1: 1, sk2: 1}的集合test.group_targeting_compound上复验了全部规则并额外补充了单字段分片键不具备的场景6.1 可以完全下推的场景_id等于复合分片键_id: {sk0, sk1, sk2}完全下推$willBeMerged: false且可搭配累积器{ $group : { _id : { sk0 : $sk0, sk1 : $sk1, sk2 : $sk2 }, sumSk1 : { $sum : $sk1 }, setSk2 : { $addToSet : $sk2 } } }结果中sumSk1与setSk2均由分片端正确计算。_id等于复合分片键 $bottom累积器root: {$bottom: {sortBy: {sk0,sk1,sk2}, output: $$ROOT}}同样完全下推说明累积器含窗口类累积器$top/$bottom不构成下推障碍。简单重命名$project将sk0/sk1/sk2分别改名为sk0Renamed/sk1Renamed/sk2Renamed后再按重命名后的字段分组依然完全下推。超集 简单重命名_id由重命名后的分片键字段再加上otherField构成完全下推。连续两个$group第二个$group按$_id原样聚合时两个组都标记$willBeMerged: false双双下推。含有非分片键字段但后续又收敛回来先$addFields添加随机字段complex: {$rand: {}}第一个$group的_id同时包含分片键字段与complex此时因为含complex它是分片键的超集仍可完全下推并标记$willBeMerged: false第二个$group只按_id.sk0/sk1/sk2聚合也完全下推。这验证了分组键包含分片键即可下推这一判据在复合键下同样成立。6.2 只能部分下推的场景非简单重命名sk1Renamed: {$add: [1, $sk1]}对分片键字段做了算术变换改变了取值不再与分片键等价因此只部分下推结果中sk1全部加 1。_id是分片键的子集subset_id: {sk0, sk2}丢弃了sk1。此时同一分组键可能横跨多个分片例如sk1不同但sk0/sk2相同只能部分下推。6.3 复合键用例小结复合分片键的黄金输出与单字段分片键完全同构所有 explain 的shardsPart/mergerPart/mergeType字段保持一致mergeType: router下推用例在合并端只有$mergeCursors唯一区别是集合索引变为[ _id_, sk0_1_sk1_1_sk2_1 ]以及底层扫描的nss变为test.group_targeting_compound。这证明下推判据与分片键的维度无关只与分组键与分片键的取值关系有关。七、规则总结与实战指引7.1 一张表看懂何时下推综合全文 26 个黄金用例可将判定规则归纳如下分组键_id形态下推程度分片端计划特征合并端特征等于分片键完全下推DISTINCT_SCAN可走索引$groupByDistinctScan或GROUP SHARDING_FILTER仅$mergeCursors分片键的简单重命名完全下推同上同上分片键的超集含其他字段/_id完全下推GROUP SHARDING_FILTER仅$mergeCursors分片键的简单重命名 超集完全下推GROUP SHARDING_FILTER仅$mergeCursors分片键经表达式派生$min/$add等部分下推$group无$willBeMerged$mergeCursors$doingMerge: true的$group非分片键字段 / 改名伪装 /$$ROOT不下推分片端仅扫描与投影路由端完整合并分组分片键被投影删除或覆盖不下推分片端扫描路由端完整合并分组分组键是分片键但 collation 非简单不下推分片端扫描路由端按 collation 合并分组分片键的子集复合键场景部分下推GROUP SHARDING_FILTER$doingMerge: true的$group7.2 三条核心判据从源码与黄金输出中可以提炼出优化器做下推决策的三条核心判据取值等价性分组键的取值必须完全由分片键决定——要么就是分片键本身要么是分片键的简单别名/超集。任何对分片键的算术、字符串或依赖其他字段的变换都会打破这种等价性退化为部分下推。字段存活性分组键所引用的分片键字段必须在该$group阶段仍然存在且未被覆盖。$project删除、$addFields覆盖都会使优化器拒绝下推。排序规则一致性聚合 collation 必须与分片键的二进制排序一致即简单 collation。非简单 collation 下分片键值可能被折叠为同一分组键下推会产生错误分组必须由路由端兜底。7.3 explain 字段速查阅读黄金输出时以下字段是判断下推与否的路标$willBeMerged: false该$group在分片端已收敛完毕不需要参与路由端合并——完全下推的标志$doingMerge: true该$group是在路由端对分片结果做最终合并——部分下推/不下推的标志mergeType: router合并发生在 mongos 上相对于在某个分片上再分发合并$groupByDistinctScanSBE 将去重型$group直接转化为基于DISTINCT_SCAN的执行节点是分组下推场景下的极致优化queryShapeHash查询形状哈希用于计划缓存与查询形状匹配同一形状的查询共享该值。7.4 对查询作者的实际建议将分组键设计为分片键或其超集让$group完全下推可显著减少跨分片数据传输与路由端合并开销优先让分片键保持干净不要在$group之前用$project/$addFields删除、覆盖或变换分片键字段避免非简单 collation 下做看起来可下推的分组否则会丢失下推收益对依赖窗口累积器$top/$bottom与多个连续$group的管道可通过 explain 中的$willBeMerged/$doingMerge逐个节点核对下推粒度。八、如何在本地复现与扩展这些黄金用例这些黄金测试是 MongoDB 源码仓库自带的集成测试可以直接在本地运行# 使用 resmoke 运行分片黄金测试 python3 buildscripts/resmoke.py run --suitesquery_golden_sharding jstests/query_golden_sharding/group_targeting.js运行后group_targeting.js会依据当前特性标志组合将实际输出与jstests/query_golden_sharding/expected_output/下对应目录如sbeRestricted/的 Markdown 文件进行比对如果输出与黄金文件不一致测试即失败并给出差异。因此这份文档既是可读的执行计划说明书又是防止执行计划意外回归的自动化检查项。若想观察不同引擎/特性下的计划差异可对比阅读同一测试在sbeDisabled/、sbeFull/、featureFlagSbeFull/、featureFlagSbeAccumulatorExpressions/等目录下的黄金输出。测试脚本头部标注的标签如featureFlagGetExecutorDeferredEngineChoice、featureFlagShardFilteringDistinctScan、requires_fcv_82说明了当前输出所依赖的特性前提而黄金输出的生成与格式化逻辑位于 jstests/libs/query/golden_test_utils.jsoutputAggregationPlanAndResults负责输出 Pipeline、Options、Results、索引清单与规整后的 explain以及 jstests/libs/query/pretty_md.js负责 Markdown 排版。最后提醒文中所引全部输出均为仓库当前固化的黄金结果随着执行引擎与特性标志的演进计划形态尤其$groupByDistinctScan的启用范围可能发生变化任何与本文不一致的现状均以仓库中jstests/query_golden_sharding/expected_output/下实时生成的黄金文件为准。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表