Drois

Apache Doris 是一个现代化的MPP(Massively Parallel Processing,即大规模并行处理) 分析型数据库产品。仅需亚秒级响应时间即可获得查询结果,有效地支持实时数据分析。 Apache Doris 的分布式架构非常简洁,易于运维,并且可以支持 10PB 以上的超大数据集。

一、Drois架构

Doris 的架构很简洁,只设 FE(Frontend)、BE(Backend)两种角色、两个进程,不依赖于 外部组件,方便部署和运维,FE、BE 都可线性扩展。

FE(Frontend):存储、维护集群元数据;负责接收、解析查询请求,规划查询计划,调度查询执行,返回查询结果。主要有三个角色:

  1. Leader 和 Follower:主要是用来达到元数据的高可用,保证单节点宕机的情况下, 元数据能够实时地在线恢复,而不影响整个服务。
  2. Observer:用来扩展查询节点,同时起到元数据备份的作用。如果在发现集群压力 非常大的情况下,需要去扩展整个查询的能力,那么可以加 observer 的节点。observer 不 参与任何的写入,只参与读取。

BE(Backend):负责物理数据的存储和计算;依据 FE 生成的物理计划,分布式地执行查询。

        数据的可靠性由 BE 保证,BE 会对整个数据存储多副本或者是三副本。副本数可根据 需求动态调整。

二、数据(表)模型

       Palo 的表中分 Key 列、Value;其中Key列在前,Value列在后且需要执行具体的聚合方式;Key列版本内有序,根据 Palo 会根据 Key 列队数据进行排序,便于快速查找;

1. 稀疏索引

        Palo 中使用类似 前缀索引 的结构来提高查询性能。数据在 Palo 内部组织为一个个 Data Block。每个 Data Block 的第一行的前几列会被用作这个 Data Block 的索引,在数据导入时被创建。考虑到索引大小等因素,Palo 最多使用一行的前 36 个字节作为索引并且遇到 VARCHAR 类型则会中断并截止。并且 VARCHAR 类型最多只是用字符串的前 20 个字节。

下面举例说明:

2. 数据模型

        Palo 中提供三种模型:AGGREGATE、 UNIQUE、 DUPLICATE,Palo 会根据对应的模型对表中相同的key做相应的处理;当选择AGGREGATE模型时,全局key唯一,会根据相同的key进行聚合操作,包括SUMREPLACEMAXMIN

AGGREGATE KEY:key列相同的记录,value列按照指定的聚合类型进行聚合,适合报表、多维分析等业务场景。

UNIQUE KEY:key列相同的记录,value列按导入顺序进行覆盖,适合按key列进行增删改查的点查询业务。

DUPLICATE KEY:key列相同的记录,同时存在于Palo中,适合存储明细数据或者数据无聚合特性的业务场景。

        注:其中 DUPLICATE 模型适合数据既没有主键,也没有聚合需求的场景,DUPLICATE KEY 只能指明底层数据排序顺序,而 UNIQUE KEY 能保证 KEY 列值得唯一性;

比如,Palo中有一张表包含三列:k1,k2和v,其中v是int类型的value列,聚合方法是SUM,k1和k2是key列。假如原本有数据如下:

3. 物化视图(Rollup)

        Rollup可以理解为 Table 的一个物化索引结构物化 是因为其数据在物理上是独立存储的。而 索引 的意思是,Rollup 存在的目是用于加速在这个 Table 上的某类查询响应。Rollup 附属于 Table,一个 Table 可以有多个 Rollup。在创建 Table 时会默认生成一个 Base Rollup,该 Rollup 包含 Table 的所有列。后续的其他 Rollup 在此基础上创建。并且通常其他 Rollup 的列数要少于 Base Rollup。如下图所示:

三、存储结构

        Doris高效的导入、查询离不开其存储结构精巧的设计。存储层对存储数据的管理通过 storage_root_path 路径进行配置,路径可以是多个。存储目录下一层按照分桶进行组织,分桶目录下存放具体的 tablet,按照 tablet_id 命名子目录。

        Segment 文件存放在 tablet_id 目录下按 SchemaHash 管理。Segment 文件可以有多个,一般按照大小进行分割,默认为 256MB。其中,Segment v2 文件命名规则为:${rowset_id}_${segment_id}.dat。具体存储目录存放格式如下图所示:

        新版Segment V2存储格式参考了Parquet的设计思路,引入了基于Page的最小数据存储单元,并将数据文件划分为数据区、索引区和元数据区三个部分。针对不同的列类型、索引格式实现了不同的Page编码方式,显著提升了数据的读写效率,并增强了数据格式的可扩展性。

Data Region:用于存储各个列的数据信息,这里的数据是按需分page加载的。

Index Region:Doris中将各个列的index数据统一存储在Index Region,这里的数据会按照列粒度进行加载,所以跟列的数据信息分开存储。

Footer信息:

  • SegmentFooterPB:定义文件的元数据信息
  • 4个字节的FooterPB内容的checksum
  • 4个字节的FileFooterPB消息长度,用于读取FileFooterPB
  • 8个字节的MAGIC CODE,之所以在末位存储,是方便不同的场景进行文件类型的识别

1. Data Region

 压缩与编码:针对不同的字段类型采用了不同的编码,默认采用LZ4F格式对数据进行压缩。

2. Index Region

2.1 Ordinal Index(一级索引)

        Ordinal Index 索引提供了通过行号来查找Column Data Page数据页的物理地址。Ordinal Index能够将按列存储数据按行对齐,可以形象理解为所谓的一级索引。

        Column Data Page是由Ordinal index进行管理,Ordinal index记录了每个Column Data Page的位置offset、大小size和第一个数据项行号信息,即Ordinal。这样每个列具有按行信息进行快速扫描的能力。

2.2 Short Key Index(前缀索引)

        数据查询时,会打开Segment文件,从footer中获取Short Key Page的offset以及大小,然后从Segment文件中读取Short Key Page中的索引数据,并解析出每一条前缀索引项。如果查询过滤条件包含前缀字段时,就可以使用前缀索引进行快速地行过滤。

        前缀索引又是稀疏索引,不能精确定位到key所在的行,只能粗粒度地定位出key可能存在的范围,然后使用二分查找算法精确地定位key的位置。

2.3 ZoneMap Index

        ZoneMap索引存储了Segment和每个列对应每个Page的统计信息。这些统计信息可以帮助在查询时提速,减少扫描数据量,统计信息包括了Min最大值、Max最小值、HashNull空值、HasNotNull不全为空的信息。

2.4 Bloom Filte Index

        Doris支持对取值区分度比较大的字段添加Bloom Filter索引。数据查询时,查询条件在设置有Bloom Filter索引的字段进行过滤,当某个Data Page的Bloom Filter没有命中时,表示该Data Page中没有需要的数据,这样可以对Data Page进行快速过滤,减少不必要的数据读取。

2.5 BitMap Index

        为了加速数据查询,Doris可以为某些字段添加Bitmap索引。

  • 有序字典:有序保存一列中所有的不同取值。
  • 字典值的位图:保存有序字典中每一个取值的位图,表示字典值在列中的行号。

3. Footer

        Footer信息段在文件的尾部,存储了文件的整体结构,包括数据域的位置,索引域的位置等信息,其中有SegmentFooterPB,CheckSum,Length,MAGIC CODE 4个部分。

索引的查询流程:

        在查询一个 Segment 中的数据时,根据执行的查询条件,会对首先根据字段加索引的情况对数据进行过滤。然后在进行读取数据,整体的查询流程如下:

  1. 首先,会按照 Segment 的行数构建一个 row_bitmap,表示记录那些数据需要进行读取,没有使用任何索引的情况下,需要读取所有数据。
  2. 当查询条件中按前缀索引规则使用到了 key 时,会先进行 ShortKey Index 的过滤,可以在 ShortKey Index 中匹配到的 ordinal 行号范围,合入到 row_bitmap 中。
  3. 当查询条件中列字段存在 BitMap Index 索引时,会按照 BitMap 索引直接查出符合条件的 ordinal 行号,与 row_bitmap 求交过滤。这里的过滤是精确的,之后去掉该查询条件,这个字段就不会再进行后面索引的过滤。
  4. 当查询条件中列字段存在 BloomFilter 索引并且条件为等值(eq,in,is)时,会按 BloomFilter 索引过滤,这里会走完所有索引,过滤每一个 Page 的 BloomFilter,找出查询条件能命中的所有 Page。将索引信息中的 ordinal 行号范围与 row_bitmap 求交过滤。
  5. 当查询条件中列字段存在 ZoneMap 索引时,会按 ZoneMap 索引过滤,这里同样会走完所有索引,找出查询条件能与 ZoneMap 有交集的所有 Page。将索引信息中的 ordinal 行号范围与 row_bitmap 求交过滤。
  6. 生成好 row_bitmap 之后,批量通过每个 Column 的 OrdinalIndex 找到到具体的 Data Page。
  7. 批量读取每一列的 Column Data Page 的数据。在读取时,对于有 null 值的 page,根据 null 值位图判断当前行是否是 null,如果为 null 进行直接填充即可。

四、分区方式

        在 Doris 的存储引擎中,用户数据被水平划分为若干个数据分片(Tablet,也称作数据分桶)。每个 Tablet 包含若干数据行。各个 Tablet 之间的数据没有交集,并且在物理上是独立存储的。
        多个 Tablet 在逻辑上归属于不同的分区(Partition)。一个 Tablet 只属于一个 Partition。而一个 Partition 包含若干个 Tablet。因为 Tablet 在物理上是独立存储的,所以可以视为 Partition 在物理上也是独立。Tablet 是数据移动、复制等操作的最小物理存储单元。若干个 Partition 组成一个 Table。Partition 可以视为是逻辑上最小的管理单元。数据的导入与删除,都可以或仅能针对一个 Partition 进行。

分区与分桶

        Doris 支持两层的数据划分。第一层是 Partition,支持 Range 和 List 的划分方式。第二层是 Bucket(Tablet),仅支持 Hash 的划分方式。
也可以仅使用一层分区。使用一层分区时,只支持 Bucket 划分。

1. Partition

  • Partition 列可以指定一列或多列。分区类必须为 KEY 列。多列分区的使用方式在后面 多列分区 小结介绍。
  • 不论分区列是什么类型,在写分区值时,都需要加双引号。
  • 分区数量理论上没有上限。
  • 当不使用 Partition 建表时,系统会自动生成一个和表名同名的,全值范围的 Partition。该 Partition 对用户不可见,并且不可删改。

 2. Bucket

        分桶是更细粒度的划分数据的方式。只支持Hash的划分方式。通过对分桶列进行hash操作,从而分散数据,将同一个hash值的数据存放在一个桶中。

  1. 如果使用了 Partition,则 DISTRIBUTED ... 语句描述的是数据在各个分区内的划分规则。如果不使用 Partition,则描述的是对整个表的数据的划分规则。
  2. 分桶列可以是多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。
  3. 分桶列的选择,是在 查询吞吐查询并发:如果选择多个分桶列,则数据分布更均匀。如果一个查询条件不包含所有分桶列的等值条件,那么该查询会触发所有分桶同时扫描,这样查询的吞吐会增加,单个查询的延迟随之降低。这个方式适合大吞吐低并发的查询场景。如果仅选择一个或少数分桶列,则对应的点查询可以仅触发一个分桶扫描。此时,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各个查询之间的IO影响较小(尤其当不同桶分布在不同磁盘上时),所以这种方式适合高并发的点查询场景。
  4. 分桶的数量理论上没有上限。

关于 Partition 和 Bucket 的数量和数据量的建议。

1. 一个表的 Tablet 总数量等于 (Partition num * Bucket num)。
2. 一个表的 Tablet 数量,在不考虑扩容的情况下,推荐略多于整个集群的磁盘数量。
3. 单个 Tablet 的数据量理论上没有上下界,但建议在 1G - 10G 的范围内。如果单个 Tablet 数据量过小,则数据的聚合效果不佳,且元数据管理压力大。如果数据量过大, 则不利于副本的迁移、补齐,且会增加 Schema Change 或者 Rollup 操作失败重试的代价 (这些操作失败重试的粒度是 Tablet)。
4. 当 Tablet 的数据量原则和数量原则冲突时,建议优先考虑数据量原则。
5. 在建表时,每个分区的 Bucket 数量统一指定。但是在动态增加分区时可以单独指定新分区的 Bucket 数量。可以利用这个功能方便的应对数据缩小或膨胀。
6. 一个 Partition 的 Bucket 数量一旦指定,不可更改。所以在确定 Bucket 数量时, 需要预先考虑集群扩容的情况。比如当前只有 3 台 host,每台 host 有 1 块盘。如果 Bucket 的数量只设置为 3 或更小,那么后期即使再增加机器,也不能提高并发度。

        举例:假设在有 10 台 BE,每台 BE 一块磁盘的情况下。如果一个表总大小为 500MB,则可以考虑 4-8 个分片。5GB:8-16 个。50GB:32 个。500GB:建议分区, 每个分区大小在 50GB 左右,每个分区 16-32 个分片。5TB:建议分区,每个分区大小在 50GB 左右,每个分区 16-32 个分片。

五、元信息管理

        Doris的FE需要管理元信息,BE只是负责计算,无需保存元信息。

        Doris 的元数据是全内存的每个 FE 内存中,都维护一个完整的元数据镜像。FE 节点分为 follower 和 observer 两类。各个 FE 之间,通过 bdbje(BerkeleyDB Java Edition)进行 leader 选举,数据同步等 工作。follower 节点通过选举,其中一个 follower 成为 leader 节点,负责元数据的写入操作。当 leader 节点宕机后,其他 follower 节点会重新选举出一个 leader,保证服务的高可用。observer 节点 仅从 leader 节点进行元数据同步,不参与选举。可以横向扩展以提供元数据的读服务的扩展性。

六、数据导入

        导入(Load)功能就是将用户的原始数据导入到 Doris 中。导入成功后,用户即可通过 Mysql 客户端查询数据。为适配不同的数据导入需求,Doris 系统提供了 6 种不同的导入方式。每种导入方式支持不同的数据源,存在不同的使用方式(异步,同步)。

1. Broker load

        通过 Broker 进程访问并读取外部数据源(如 HDFS)导入到 Doris。用户通过 Mysql协议提交导入作业后,异步执行。通过 SHOW LOAD 命令查看导入结果。

2. Stream load

        用户通过 HTTP 协议提交请求并携带原始数据创建导入。主要用于快速将本地文件或数据流中的数据导入到 Doris。导入命令同步返回导入结果。


3. Insert

        类似 MySQL 中的 Insert 语句,Doris 提供 INSERT INTO tbl SELECT ...; 的方式从 Doris 的表中读取数据并导入到另一张表。或者通过 INSERT INTO tbl VALUES(...);插入单 条数据。


4. Multi load

        用户通过 HTTP 协议提交多个导入作业。Multi Load 可以保证多个导入作业的原子生 效。

5. Routine load
        用户通过 MySQL 协议提交例行导入作业,生成一个常驻线程,不间断的从数据源(如Kafka)中读取数据并导入到 Doris 中。

6. 通过 S3 协议直接导入

        用户通过 S3 协议直接导入数据,用法和 Broker Load 类似。Broker load 是一个异步的导入方式,支持的数据源取决于 Broker 进程支持的数据源。 用户需要通过 MySQL 协议创建 Broker load 导入,并通过查看导入命令检查导入结果。

        所有导入方式都支持 csv 数据格式。其中Broker load还支持parquet和orc 数据格式。不同的导入方式适用于不同的场景。一下主要介绍Broker load和Stream load两种常见的导入方式:

1.Stream load

        Stream load 是一个同步的导入方式,用户通过发送 HTTP 协议发送请求将本地文件或 数据流导入到 Doris 中。Stream load 同步执行导入并返回导入结果。用户可直接通过请求 的返回体判断本次导入是否成功。

        Stream load 主要适用于导入本地文件,或通过程序导入数据流中的数据。目前 Stream Load 支持两个数据格式:CSV(文本)和 JSON。下图展示了 Stream load 的主要流程,省略了一些导入细节。

        Stream load 中,Doris 会选定一个节点作为 Coordinator 节点。该节点负责接数据并分发数据到其他数据节点。用户通过 HTTP 协议提交导入命令。如果提交到 FE,则 FE 会通过 HTTP redirect 指 令将请求转发给某一个 BE。用户也可以直接提交导入命令给某一指定 BE。导入的最终结果由 Coordinator BE 返回给用户。

数据导入的流程如下:

  1. 用户选择一台BE作为协调者Coordinator,发起数据导入请求,传入数据格式,数据源和标识此次数据导入的label,label用于避免数据重复导入。如果提交到 FE,则 FE 会通过 HTTP redirect 指令将请求转发给某一个 BE;
  2. Coordinator收到请求后,向FE master节点上报,执行loadTxnBegin, 创建全局事务。 因为导入过程中,需要同时更新base表和物化索引的多个bucket, 为了保证数据导入的一致性,用事务控制本次导入的原子性;
  3. Coordinator创建事务成功后, 执行streamLoadPut调用, 从FE获得本次数据导入的计划。 数据导入, 可以看成是将数据分发到所涉及的全部的tablet副本上, BE从FE获取的导入计划包含数据的schema信息和tablet副本信息;
  4. Coordinator从数据源拉取数据, 根据base表和物化索引表的schema信息, 构造内部数据格式;
  5. Coordinator根据分区分桶的规则和副本位置信息,将发往同一个BE的数据,批量打包,发送给BE,BE收到数据后,将数据写入到对应的tablet副本中;
  6. 当Coordinator节点完成此次数据导入,向FE master节点执行loadTxnCommit,提交全局事务,发送本次数据导入的执行情况,FE master确认所有涉及的tablet的多数副本都成功完成,则发布本次数据导入使数据对外可见,否则导入失败,数据不可见,后台负责清理掉不一致的数据;

 2. Broker load

       Broker load 是一个异步的导入方式,支持的数据源取决于 Broker 进程支持的数据源。用户需要通过 MySQL 协议 创建 Broker load 导入,并通过查看导入命令检查导入结果。Broker 以插件的形式,独立于 Doris 部署。如果需要从第三方存储系统导入数据,需要部署相应的 broker。

        用户在提交导入任务后,FE 会生成对应的 Plan 并根据目前 BE 的个数和文件的大小,将 Plan 分给多个 BE 执行,每个 BE 执行一部分导入数据。BE 在执行的过程中会从 Broker 拉取数据,在对数据 transform 之后将数据导入系统。所有 BE 均完成导入,由 FE 最终决定导入是否成功。

七、数据查询

        用户可使用MySQL客户端连接FE,执行SQL查询, 获得结果。

  1. MySQL客户端执行DQL SQL命令。
  2. FE解析, 分析, 改写, 优化和规划, 生成分布式执行计划。
  3. 分布式执行计划由 若干个可在单台be上执行的plan fragment构成, FE执行exec_plan_fragment, 将plan fragment分发给BE,指定其中一台BE为coordinator。
  4. BE执行本地计算, 比如扫描数据。
  5. 其他BE调用transimit_data将中间结果发送给BE coordinator节点汇总为最终结果。
  6. FE调用fetch_data获取最终结果。
  7. FE将最终结果发送给MySQL client。

        执行计划在BE上的实际执行过程比较复杂,,采用向量化执行方式,比如一个算子产生4096个结果,输出到下一个算子参与计算,而非batch方式或者one-tuple-at-a-time。

八、Drois SQL 原理解析

        SQL解析在下文中指的是将一条sql语句经过一系列的解析最后生成一个完整的物理执行计划的过程。

        Doris SQL解析具体包括了五个步骤:词法分析,语法分析,生成单机逻辑计划,生成分布式逻辑计划,生成物理执行计划。

具体代码实现上包含以下五个步骤:Parse、Analyze、SinglePlan、DistributedPlan、Schedule。

下图展示了一个简单的查询SQL在Doris的解析实现:

1. Parse阶段

词法分析采用jflex技术,语法分析采用java cup parser技术,最后生成抽象语法树(Abstract Syntax Tree)AST,这些都是现有的、成熟的技术,在这里不进行详细介绍。

        AST是一种树状结构,代表着一条SQL。不同类型的查询select, insert, show, set, alter table, create table等经过Parse阶段后生成不同的数据结构(SelectStmt, InsertStmt, ShowStmt, SetStmt, AlterStmt, AlterTableStmt, CreateTableStmt等),但他们都继承自Statement,并根据自己的语法规则进行一些特定的处理。例如:对于select类型的 sql, Parse之后生成了SelectStmt结构。

        SelectStmt结构包含了SelectList,FromClause,WhereClause,GroupByClause, SortInfo等结构。这些结构又包含了更基础的一些数据结构,如WhereClause包含了BetweenPredicate (between表达 式), BinaryPredicate(二元表达式), CompoundPredicate(and or组合表达式), InPredicate(in表达式)等。

2. Analyze阶段

抽象语法树是由StatementBase这个抽象类表示。这个抽象类包含一个最重要的成员函数analyze(),用来执行Analyze阶段要做的事。

        不同类型的查询select, insert, show, set, alter table, create table等经过Parse阶段后生成不同的数据结构(SelectStmt, InsertStmt, ShowStmt, SetStmt, AlterStmt, AlterTableStmt, CreateTableStmt等),这些数据结构继承自StatementBase,并实现analyze()函数,对特定类型的SQL进行特定的 Analyze。

例如:select类型的查询,会转成对select sql的子语句SelectList, FromClause, GroupByClause, HavingClause, WhereClause, SortInfo等的analyze()。然后这些子语句再各自对自己的子结构进行进一步的analyze(),通过层层迭代,把各种类型的sql的各种 情景都分析完毕。例如:WhereClause进一步分析其包含的BetweenPredicate(between表达式), BinaryPredicate(二元表达式), CompoundPredicate(and or组合表达式), InPredicate(in表达式)等。

3. 生成单机逻辑Plan阶段

这部分工作主要是根据AST抽象语法树生成代数关系,也就是俗称的算子数。树上的每个节点都是一个算子,代表着一种操作。

        ScanNode代表着对一个表的扫描操作,将一个表的数据读出来。HashJoinNode代表着join操作,小表在内存中构建哈希表,遍历大表找到连接键相同的值。Project表示投影操作,代表着最后需要输出的列,图片表示只用输出citycode这一列。

4. 生成分布式Plan阶段

        有了单机的PlanNode树之后,就需要进一步根据分布式环境,拆成分布式PlanFragment树(PlanFragment用来表示独立的执行单元),毕竟一个表的数据分散地存储在多台主机上,完全可以让一些计算并行起来。

        这个步骤的主要目标是最大化并行度和数据本地化。主要方法是将能够并行执行的节点拆分出去单独建立一个PlanFragment,用 ExchangeNode代替被拆分出去的节点,用来接收数据。拆分出去的节点增加一个DataSinkNode,用来将计算之后的数据传送到 ExchangeNode中,做进一步的处理。

        这一步采用递归的方法,自底向上,遍历整个PlanNode树,然后给树上的每个叶子节点创建一个PlanFragment,如果碰到父节点,则考 虑将其中能够并行执行的子节点拆分出去,父节点和保留下来的子节点组成一个parent PlanFragment。拆分出去的子节点增加一个父节点DataSinkNode组成一个child PlanFragment,child PlanFragment指向parent PlanFragment。这样就确定了数据的流动方向。

5. Schedule阶段

这一步是根据分布式逻辑计划,创建分布式物理计划。主要解决以下问题:

  •     哪个 BE 执行哪个 PlanFragment
  •     每个 Tablet 选择哪个副本去查询
  •     如何进行多实例并发

九、SQL兼容性

        Doirs的sql符合sql标准,另外join时无需感知数据的分布。Doris还支持shuffer join和不相关子查询。Doris在建表时指定两表的分布时,也支持colocation join,比较适合分桶方式相同的两个大表的join。

1. Colocation Join

        Colocation Join 功能,是将一组拥有相同 CGS 的 Table 组成一个 CG。并保证这些 Table 对应的数据分片会落在同一个 BE 节点上。使得当 CG 内的表进行分桶列上的 Join 操作时,可以通过直接进行本地数据 Join,减少数据在节点间的传输耗时。
一个表的数据,最终会根据分桶列值 Hash、对桶数取模的后落在某一个分桶内。假设一个 Table 的分桶数为 8,则共有 [0, 1, 2, 3, 4, 5, 6, 7] 8 个分桶(Bucket),我们称这样一个序列为一个 BucketsSequence。每个 Bucket 内会有一个或多个数据分片(Tablet)。当表为单分区表时,一个 Bucket 内仅有一个 Tablet。如果是多分区表,则会有多个。
为了使得 Table 能够有相同的数据分布,同一 CG 内的 Table 必须保证以下属性相同:

  1. 分桶列和分桶数。
    分桶列,即在建表语句中 DISTRIBUTED BY HASH(col1, col2, ...) 中指定的列。分桶列决定了一张表的数据通过哪些列的值进行 Hash 划分到不同的 Tablet 中。同一 CG 内的 Table 必须保证分桶列的类型和数量完全一致,并且桶数一致,才能保证多张表的数据分片能够一一对应的进行分布控制。
  2. 副本数。
    同一个 CG 内所有表的所有分区(Partition)的副本数必须一致。如果不一致,可能出现某一个 Tablet 的某一个副本,在同一个 BE 上没有其他的表分片的副本对应。
    同一个 CG 内的表,分区的个数、范围以及分区列的类型不要求一致。
    在固定了分桶列和分桶数后,同一个 CG 内的表会拥有相同的 BucketsSequence。而副本数决定了每个分桶内的 Tablet 的多个副本,存放在哪些 BE 上。假设 BucketsSequence 为 [0, 1, 2, 3, 4, 5, 6, 7],BE 节点有 [A, B, C, D] 4个。则一个可能的数据分布如下:

 CG 内所有表的数据都会按照上面的规则进行统一分布,这样就保证了,分桶列值相同的数据都在同一个 BE 节点上,可以进行本地数据 Join。

2. Bucket Shuffle Join

Doris支持的常规分布式Join方式包括了shuffle join 和 broadcast join。这两种 join 都会导致不小的网络开销:
举个例子,当前存在A表与B表的Join查询,它的Join方式为HashJoin,不同Join类型的开销如下:

  • Broadcast Join: 如果根据数据分布,查询规划出A表有3个执行的HashJoinNode,那么需要将B表全量的发送到3个HashJoinNode,那么它的网络开销是3B,它的内存开销也是3B
  • Shuffle Join: Shuffle Join会将A,B两张表的数据根据哈希计算分散到集群的节点之中,所以它的网络开销为 A + B,内存开销为 B

        在 FE 之中保存了 Doris 每个表的数据分布信息,如果 join 语句命中了表的数据分布列,我们应该使用数据分布信息来减少 join 语句的网络与内存开销,这就是 Bucket Shuffle Join 的思路来源。

        上面的图片展示了 Bucket Shuffle Join 的工作原理。SQL 语句为 A 表 join B 表,并且 join 的等值表达式命中了 A 的数据分布列。而 Bucket Shuffle Join 会根据 A 表的数据分布信息,将 B 表的数据发送到对应的 A 表的数据存储计算节点。Bucket Shuffle Join 开销如下:

  • 网络开销: B < min(3B, A + B)
  • 内存开销: B <= min(3B, B)

        可见,相比于 Broadcast Join 与 Shuffle Join, Bucket Shuffle Join 有着较为明显的性能优势。减少数据在节点间的传输耗时和 Join 时的内存开销。相对于 Doris 原有的 Join 方式,它有着下面的优点:

  • Bucket-Shuffle-Join 降低了网络与内存开销,使一些 Join 查询具有了更好的性能。尤其是当 FE 能够执行左表的分区裁剪与桶裁剪时。
  • 同时与 Colocate Join 不同,它对于表的数据分布方式并没有侵入性,这对于用户来说是透明的。对于表的数据分布没有强制性的要求,不容易导致数据倾斜的问题。
  • 它可以为 Join Reorder 提供更多可能的优化空间。

3. Runtime Filter

        Runtime Filter 在查询规划时生成,在 HashJoinNode 中构建,在 ScanNode 中应用。
举个例子,当前存在 T1表与 T2表的 Join 查询,它的 Join 方式为 HashJoin,T1是一张事实表,数据行数为100000,T2是一张维度表,数据行数为2000,Doris join 的实际情况是:

        显而易见对 T2扫描数据要远远快于 T1,如果我们主动等待一段时间再扫描 T1,等 T2将扫描的数据记录交给 HashJoinNode 后,HashJoinNode 根据 T2的数据计算出一个过滤条件,例如 T2数据的最大和最小值,或者构建一个 Bloom Filter,接着将这个过滤条件发给等待扫描 T1的 ScanNode,后者应用这个过滤条件,将过滤后的数据交给 HashJoinNode,从而减少 probe hash table 的次数和网络开销,这个过滤条件就是 Runtime Filter,效果如下:

        如果能将过滤条件(Runtime Filter)下推到存储引擎,则某些情况下可以利用索引来直接减少扫描的数据量,从而大大减少扫描耗时,效果如下:

        可见,和谓词下推、分区裁剪不同,Runtime Filter 是在运行时动态生成的过滤条件,即在查询运行时解析 join on clause 确定过滤表达式,并将表达式广播给正在读取左表的 ScanNode,从而减少扫描的数据量,进而减少 probe hash table 的次数,避免不必要的 I/O 和网络传输。
Runtime Filter 主要用于优化针对大表的 join,如果左表的数据量太小,或者右表的数据量太大,则 Runtime Filter 可能不会取得预期效果。 

参考:云数据仓库 Doris 版 Colocation Join-操作指南-文档中心-腾讯云-腾讯云

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值