尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hazelcast 分布式 SQL 扫描设计解析:访问路径选择、本地执行与集群重配置下的正确性保障

Hazelcast 分布式 SQL 扫描设计解析:访问路径选择、本地执行与集群重配置下的正确性保障 缓存KV存储消息队列流处理后端【免费下载链接】hazelcastHazelcast is a unified real-time data platform combining stream processing with a fast data store, allowing customers to act instantly on>项目地址https://gitcode.com/gh_mirrors/ha/hazelcast点击查看免费下载Hazelcast Mustang 是 Hazelcast 的分布式 SQL 查询引擎一条 SQL 语句被转换为包含scan、project、filter等关系运算符的查询计划树树的叶子算子scan负责遍历底层IMap数据。本文基于 docs/design/sql/10-distributed-scan.md 这份设计文档深入讲解扫描算子的三个核心设计点——访问路径选择、本地执行语义、以及对集群重配置的应对并结合当前仓库源码验证这些机制的实际落地方式。读完本文你将理解 Hazelcast 如何在直接扫描 IMap与走二级索引之间做代价决策、扫描算子如何流式产出结果以及分区迁移与对象销毁并发发生时引擎如何保证结果一致性。说明该设计文档标题中标注了 Outdated since Hazelcast 5.0。它描述的是 Mustang 引擎5.0 之前中MapScanExec/MapIndexScanExec时代的设计5.0 之后 SQL 执行迁移到了 Jet 处理器框架如MapIndexScanP但访问路径选择、分区正确性校验等核心设计思想在 IndexResolver.java、IndexScanMapPhysicalRel.java 与 MapIndexScanP.java 中延续至今因此本文以设计文档为主线、以当前源码为佐证。1 背景分区、IMap 与二级索引Hazelcast IMDG 是分布式内存键值存储数据存放于IMap等分布式对象中。集群有固定数量的分区默认271 个。每个分区只存储在一个成员member上并可在其他成员上拥有零个、一个或多个备份每个分布式对象被切分到若干分区上。当拓扑发生变化成员加入或离开时分区会在成员之间迁移migrate。IMap有两种访问方式直接访问按分区遍历 record store记录存储通过二级索引访问二级索引本身也是分布式结构每个成员只持有为本地条目构建的那部分索引。Mustang 引擎的目标是为每条 SQL 选择正确的访问方法直接扫描或索引扫描在成员上启动扫描并收集结果。以下各节描述其实现方式。2 访问路径选择直接扫描还是索引扫描扫描IMap有两种路径——直接遍历 record store或使用某个二级索引。路径选择发生在规划planning阶段。2.1 决策入口与规则触发规划的核心入口是IndexResolver.createIndexScans见 IndexResolver.java如果表上没有索引直接选择直接扫描不再做任何优化如果表上有索引则分析TableScan算子表中的谓词尝试为每个可用索引构造MapIndexScanPhysicalRel算子并加入规划器搜索空间。该过程由优化规则 IndexScanMapPhysicalRule.java 触发规则匹配逻辑层FullScanLogicalRel当表类型为PartitionedMapTable时取出table.getIndexes()并逐一调用IndexResolver.createIndexScans把每个候选RelNode通过call.transformTo(...)提交给规划器。2.2 谓词 CNF 拆分与候选收集第一步将谓词拆分为合取范式CNF。例如a1 AND b2被拆成a1和b2两个子谓词a1 OR b2保持不变仍是一个整体。第二步对每个子谓词判断其能否被某个索引利用。设col是简单列表达式exp是叶子处只有常量或参数即不引用其他列的表达式判定规则如下谓词形式可用的索引类型col expSORTED与HASH索引col [比较符] exp如,,,SORTED索引col IS NULL / IS TRUE / IS FALSE视作等式表达式对NULL值的语义略有不同col exp1 OR col exp2视作两个等式的并集该步骤的结果是列到候选表达式的映射。例如a1 AND b2 AND b4被表示为a - [1] b - [2], [4]在源码中这一步对应IndexResolver中一系列的prepareSingleColumnCandidate*方法包括等式/比较prepareSingleColumnCandidateComparison、prepareSingleColumnSearchCandidateComparison、IS TRUE/FALSEprepareSingleColumnCandidateBooleanIsTrueFalse与IS NULL/NOT NULLprepareSingleColumnCandidateIsNull、prepareSingleColumnCandidateIsNotNull候选数据封装在IndexComponentCandidate中。2.3 索引绑定连续前缀与最后一项等值约束第三步遍历每一个索引尝试把候选表达式绑定到索引的列上绑定依据是索引列与索引类型。通用规则如下SORTED索引可以使用等值与比较条件HASH索引只能使用等值条件只能绑定索引列的连续前缀。例如对索引(a, b, c)条件a1 AND c2只能绑定a1因为b未参与前缀在a处中断条件a1 AND b2可同时绑定a1与b2前缀中除最后一个表达式之外其余都必须是等值条件。例如索引(a, b)配条件a1 AND b2时只能使用a1因为a1是范围而非等值无法继续向后绑定b。此步骤的结果是索引到可用过滤条件的映射。例如对表达式a1 AND b2 AND b4设有索引(a, b)、(a, c)与(b)结果如下index(a, b) - [a1, b2 AND b4] index(a, c) - [a1, NULL] index(b) - [b2 AND b4]2.4 余下过滤器remainder filter索引过滤器index filter确定之后引擎计算余下过滤器remainder filter使得indexFilter AND remainderFilter 等价于 原始过滤器即索引负责缩小读取范围余下条件负责把索引无法判定的部分在读取后二次过滤掉。IndexScanMapPhysicalRel算子见 IndexScanMapPhysicalRel.java正是持有index、indexFilter、indexExp与remainderExp四个成员索引对象、索引过滤器、索引表达式与余下表达式。底层过滤条件在运行时被解析为 IndexFilter.java 体系包含IndexEqualsFilter等值、IndexRangeFilter范围与IndexCompositeFilter组合等实现。2.5 搜索空间与代价模型对于每个候选索引引擎创建一个MapIndexScanPhysicalRel加入规划器搜索空间。设计文档明确指出目前所有可用索引都会被加入搜索空间这在未来引入 join 和多表查询时可能因为备选方案过多而成为问题届时可以考虑基于启发式规则剔除部分索引。代价模型按以下步骤工作获取预计扫描行数SCANNED_ROWS直接扫描等于 map 总行数索引扫描mapRowCount * selectivity(indexFilter)按索引过滤条件的选择性折算获取预计返回行数RETURNED_ROWS取决于原始过滤条件的选择性根据访问方法给扫描行数赋权重SCAN_MULTIPLIER直接扫描比索引扫描更便宜索引扫描需要一次间接寻址HASH索引扫描比SORTED索引扫描更便宜查找所需 CPU 更少最终公式COST SCANNED_ROWS * SCAN_MULTIPLIER RETURNED_ROWS * PROJECTION_COST该公式保证了当过滤条件选择性很差时选择直接扫描选择性好时选择索引扫描且在索引之间优先HASH索引。当前源码中直接扫描算子的代价计算位于 FullScanPhysicalRel.java扫描行数乘以TABLE_SCAN_CPU_MULTIPLIER在 CostUtils.java 中定义为1.0d再叠加过滤与投影的 CPU 代价。行数估计来自table.getStatistic().getRowCount()与RelMdUtil.guessSelectivity(filter)与设计文档中按选择性折算扫描行数的思路一致。3 本地执行三要素与流式批量产出扫描算子由三部分组成访问路径access path直接扫描或索引扫描可选过滤器filter列列表column list。执行过程如下先打开底层数据的迭代器record stores 或索引对每个返回的记录先与可选过滤器求值若记录未被过滤掉则按列列表转换为Row实例并加入内部批次当批次达到一定大小、或没有更多记录时把整批Row返回给父算子。关键点在于扫描的完整结果集永远不会被物化——这与旧版 predicate engine 形成鲜明对比。直接扫描设计文档中的MapScanExec迭代器逐个扫描本地所有分区索引扫描MapIndexScanExec迭代器基于可选的索引条件对着索引打开。在 5.0 后的 Jet 处理器架构中索引扫描由 MapIndexScanP.java 实现其核心是split 机制每个 split 附着在某个具体成员上正常执行期间每个 split 只有单一实例。处理器初始假定所有分配的分区都是本地的通过MapFetchIndexOperation从本地或迁移后的新归属成员按IndexIterationPointer分页拉取索引条目indexFilterToPointers见 QueryUtil.java把IndexFilter翻译为迭代指针FETCH_SIZE_HINT_PROPERTY默认值 128 控制每批拉取规模。这正是文档所述按批次产出、绝不物化全集在现代实现中的体现。并发修改语义查询执行期间条目可能被并发修改。引擎没有事务因此同一更新条目可能被观察到0 次、1 次或多次取决于底层数据结构中条目的重定位。要彻底解决需要引入事务引擎而当前引擎没有。设计文档同时指出这种读取不可重复行为在许多数据库包括事务型数据库中都存在例如 Redis 的SCAN、MongoDB 在 read-uncommitted 级别的多文档写入语义以及 Microsoft SQL Server 默认READ_COMMITTED隔离级别下的行为原文档以脚注形式给出这些外部参考此处仅作概念对照不再展开外部链接。4 集群重配置迁移与并发 Schema 变更查询执行期间集群配置可能发生变化分区可能在成员间迁移分布式对象可能被创建或销毁。本节描述扫描算子在并发集群重配置下的行为。4.1 分区迁移如果查询与分区迁移并发执行可能出现某些分区被扫描多次、或被完全漏扫。为避免这种情况需要一种机制保证引擎对每个分区恰好观察一次。计划期快照创建计划时引擎生成所谓的partition map即成员到分区的映射并保存在计划中。如果成员加入/离开导致分区分布变化计划被失效invalidate。启动前显式分配查询即将开始时引擎显式地为每个参与者分配分区例如member1 - [1, 2] member2 - [3, 4]启动前校验扫描在某成员上启动时首先检查该成员是否持有预期分区。若预期与实际持有的分区不匹配抛出异常。例如成员被期望持有分区[1, 2][1] - 错误可能漏掉分区 [2] [1, 2] - 正常 [1, 2, 3] - 错误分区 [3] 可能被重复返回返回前复检仅仅在查询启动前检查不足以保证结果正确——分区可能在执行期间迁入导致重复或迁出导致漏扫。因此扫描算子在返回任何结果之前都会重新检查分区分布直接扫描使用迁移戳migration stamps即MapService.validateMigrationStamp见 MapService.java内部委托给CountingMigrationAwareService.validateMigrationStamp通过比对迁移计数判断分区是否发生过变动索引扫描使用索引分区戳即InternalIndex.validatePartitionStamp见 InternalIndex.java 及其GlobalIndexPartitionTracker实现。在 5.0 后的MapIndexScanP实现中迁移容忍被进一步细化分区迁移期间处理器会把原始 split 按新归属拆分成互不相交的若干部分向新成员重试拉取只有当 split 中所有分区都被完整读取后才将其从执行中移除遇到缺失分区时会短暂延迟DELAY_AFTER_MISSING_PARTITION100ms后重试。这套机制把报错重跑压缩到更少的场景。设计取舍更理想的方案是把分区问题对用户完全隐藏——但实践中很难做到。要避免重复与漏扫需要追踪算子输入中哪些部分已被处理、并在运行时把扫描重调度到其他成员。因此当前设计保证的是用户不会看到不一致的结果如果引擎无法保证结果正确性则强制用户手动重跑查询。长期目标是尽量减少乃至消除抛出错误的场景。4.2 并发 Schema 变更规划时存在的被引用对象IMap、索引可能已经不存在于本地成员上——例如 map 被并发销毁。因此扫描操作启动前会检查所需对象是否存在若对象缺失抛出异常并使计划失效。同样的检查还会在下一个结果批次返回给父算子之前再次执行以避免读取已被销毁 map 的过期 record store。这一点在MapIndexScanP中有直接对应当MapFetchIndexOperation因分区缺失而失败时MissingPartitionException处理器的响应处理逻辑会区分正常迁移重试与目标成员失联TargetNotMemberException、WrongTargetException、TargetDisconnectedException、MemberLeftException等场景保证不把不完整或过期的数据向上游输出。5 设计要点与当前仓库实现对照设计文档中的机制当前仓库中的实现相对路径索引候选生成入口IndexResolver.createIndexScansIndexResolver.java索引扫描优化规则IndexScanMapPhysicalRule.javaMapIndexScanPhysicalRel算子index/indexFilter/remainderIndexScanMapPhysicalRel.java直接扫描代价计算FullScanPhysicalRel.java、CostUtils.java索引过滤器运行时表示IndexFilter.java 及IndexEqualsFilter/IndexRangeFilter/IndexCompositeFilter索引扫描执行处理器split 机制、MapFetchIndexOperation、迁移重试MapIndexScanP.java直接扫描的迁移戳校验MapService.java索引扫描的分区戳校验InternalIndex.java、GlobalIndexPartitionTracker.java总结分布式 SQL 扫描的正确性来自三个层面的配合规划期通过 CNF 拆分与索引绑定规则枚举访问路径并用代价模型在直接扫描、HASH索引、SORTED索引之间择优执行期以迭代器 批量产出的方式流式返回结果绝不物化全集重配置期通过 partition map 快照、启动前校验、返回前复检迁移戳 / 分区戳三重防线保证每个分区恰好被观察一次。虽然设计文档标注 5.0 后过时但其机制在当前的IndexResolver、FullScanPhysicalRel与MapIndexScanP等实现中依然清晰可见——理解这份设计是深入 Hazelcast SQL 执行链路的最佳入口。赞分享缓存KV存储消息队列流处理后端【免费下载链接】hazelcastHazelcast is a unified real-time data platform combining stream processing with a fast data store, allowing customers to act instantly on>项目地址https://gitcode.com/gh_mirrors/ha/hazelcast点击查看免费下载相关推荐AI NovelGenerator 教程用大模型快速产出一部长篇小说AI NovelGenerator 教程用大模型快速产出一部长篇小说 AI NovelGenerator 是基于大语言模型的开源小说生成工具能自动产出多章节人工智能大模型AI 应用AI 写作RAG桌面应用终极指南Quartz.NET分布式锁如何确保集群环境下任务不重复执行终极指南Quartz.NET分布式锁如何确保集群环境下任务不重复执行 Quartz.NET是一个功能强大的企业级任务调度框架广泛应用于.NET应用程序中。在任务调度后端OpenMed ONNX Runtime Web Loader浏览器端本地 PII 脱敏的加载器设计与执行路径选择OpenMed ONNX Runtime Web Loader浏览器端本地 PII 脱敏的加载器设计与执行路径选择 OpenMedKit Web 内置了基于人工智能NLP医疗健康数据脱敏本地部署大模型AI 应用MCP 服务联邦学习上一篇easy-vibe 实战指南Claude Code Agent Teams 多智能体协作开发完全解析下一篇Readest TTS 测试微任务时序 flake 全解析jsdom 拆除后的 CustomEvent realm 错配根因、复现与修复创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表