【转】https://mp.weixin.qq.com/s/YNGdTbnG6iBjT1qtsK912w
Doris 简介
首先简单介绍一下 Doris 。Doris 是百度自主研发并开源的一个基于 MPP (大规模并行处理) 架构的分析型数据库,它的特点就是性能卓越,能够做到 PB 级别的数据分析的毫秒/秒级的响应,适用于高并发低延时下的实时报表、多维分析等需求场景。
Doris 最早是叫 Palo ,2017 年我们以百度 Palo 的方式在 GitHub 上进行了开源,在 2018 年的时候把它贡献给 Apache 社区,正式更名为 Apache Doris 。而在百度内部一直沿用了 Palo 的名称,并在百度智能云上提供了 Palo 的企业级托管版本。
Doris 是一个 MPP 架构的分析型数据库,有几个特点:
第一个特点,简单易用,支持标准 SQL 并且完全兼容 MySQL 协议,产品使用起来非常方便。
第二,它采用了预聚合技术、向量化执行引擎,再加上列式存储,是一个高效查询引擎,能在秒级甚至毫秒级返回海量数据下的查询结果。
第三,它的架构非常简单,只有两组进程:FE 负责管理元数据,并负责解析 SQL 、生成和调度查询计划;BE 负责存储数据以及执行 FE 生成的查询计划。
这个简洁高效的架构使得它运维、部署简单、扩展性强,能够支持大规模的计算。
通过下面这张图我们简单梳理一下 Doris 的结构,Doris 主要分为两个角色,一个是 FE,另外一个是 BE 。
从 SQL 执行的角度说, FE 在 Doris 当中承担了 MySQL 接入层,负责解析、生成、调度查询计划。BE 负责对应的查询计划的执行,负责实现实际的查询、导入等工作。从数据的角度说,FE 负责元数据的存储,比如表,数据库,用户信息等数据,BE 负责列存数据的落地存储。
这个架构是非常简洁的,每个 BE 节点它是对等的。FE 分为 Leader、Follower、Observer这几个角色,这和 ZooKeeper 之中的角色定位是类似的, Leader 跟 Follower 参与到集群选主、元数据的修改等工作,而 Observer 是不参与这个过程的,只提供数据的读取,对外提供 FE 的读扩展性,所以 FE 与 BE 节点都可以线性的扩展。
接下来是 Doris 当中数据的分布式存储机制,Doris 作为一个 MPP 数据库,它的数据存储会深刻影响到后续我们要分析的 Join 实现与调优。
Doris 可以支持多副本的存储,而且数据能够自动迁移实现副本平衡。我们看到,Doris 中的数据是以 Tablet 的形式组织的,每一个表会拆分成多个 Tablet ,每个 Tablet 是由数据分区跟数据分桶来确定的。一旦确定了 Tablet 之后,在 Doris 当中所有的数据都是基于 Tablet 来调度,我们可以看到一个 tablet 可以分散在多个 BE 上做多副本的存储,如果有 BE 节点宕机,或者是有新的 BE 节点加入时,系统也会自动在后台执行数据副本的均衡。
在查询的时候也会把查询负载均衡到所有的 BE 上,这就是 Doris 在数据副本存储上的整体架构,后面我们做 Join 分析的时候也会看到数据副本、包括数据是怎么样在当中调度的。
Doris Join 实现机制
Doris 支持两种物理算子,一类是 Hash Join,另一类是 Nest Loop Join。
-
Hash Join:在右表上根据等值 Join 列建立哈希表,左表流式的利用哈希表进行 Join 计算,它的限制是只能适用于等值 Join。
-
Nest Loop Join:通过两个 for 循环,很直观。然后它适用的场景就是不等值的 Join,例如:大于小于或者是需要求笛卡尔积的场景。它是一个通用的 Join 算子,但是性能表现差。
作为分布式的 MPP 数据库, 在 Join 的过程中是需要进行数据的 Shuffle。数据需要进行拆分调度,才能保证最终的 Join 结果是正确的。举个简单的例子,假设关系S 和 R 进行Join,N 表示参与 Join 计算的节点的数量;T 则表示关系的 Tuple 数目。
Doris 支持 4 种数据 Shuffle 方式:
BroadCast Join
它要求把右表全量的数据都发送到左表上,即每一个参与 Join 的节点,它都拥有右表全量的数据,也就是 T(R)。
它适用的场景是比较通用的,同时能够支持 Hash Join 和 Nest loop Join,它的网络开销 N * T(R)。
Shuffle Join
当进行 Hash Join 时候,可以通过 Join 列计算对应的 Hash 值,并进行 Hash 分桶。
它的网络开销则是:T(R) + T(N),但它只能支持 Hash Join,因为它是根据 Join 的条件也去做计算分桶的。
Bucket Shuffle Join
Doris 的表数据本身是通过 Hash 计算分桶的,所以就可以利用表本身的分桶列的性质来进行 Join 数据的 Shuffle。假如两张表需要做 Join,并且 Join 列是左表的分桶列,那么左表的数据其实可以不用去移动右表通过左表的数据分桶发送数据就可以完成 Join 的计算。
它的网络开销则是:T(R)相当于只 Shuffle 右表的数据就可以了。
Colocation
它与Bucket Shuffle Join相似,相当于在数据导入的时候,根据预设的 Join 列的场景已经做好了数据的 Shuffle。那么实际查询的时候就可以直接进行 Join 计算而不需要考虑数据的 Shuffle 问题了。(预shuffle)
Join 数据的 Shuffle 方式
下面这张图是 BroadCast Join,左表的数据是没有移动的,右表每一个 BE 节点扫描的数据都发送到对应的 Join 节点上,每个 Join 的计算节点上都有右表全量的数据。
第二种就是 Shuffle Join,每个数据扫描节点将数据扫出来之后进行Partition 分区,然后根据 Partition 分区的结果分别把左右表的数据发送到对应的 Join 计算节点上。
第三张图是 Bucket Shuffle Join,右表数据扫描出来之后进行数据分区的 Hash 计算,根据左表本身的数据分布发送到对应的 Join 计算节点上。
最后就是 CoLocate Join。它其实没有真正的数据 Shuffle,数据扫描之后进行 Join 计算就OK了。
Join 数据的 Shuffle 方式
上面这 4 种方式灵活度是从高到低的,它对这个数据分布的要求是越来越严格,但 Join 计算的性能也是越来越好的。
接下来就要分享的是 Doris 近期加入的一个新特性—— Runtime Filter 的实现逻辑。
Doris 在进行 Hash Join 计算时会在右表构建一个哈希表,左表流式的通过右表的哈希表从而得出 Join 结果。而 RuntimeFilter 就是充分利用了右表的 Hash 表,在右表生成哈希表的时,同时生成一个基于哈希表数据的一个过滤条件,然后下推到左表的数据扫描节点。通过这样的方式,Doris 可以在运行时进行数据过滤。
假如左表是一张大表,右表是一张小表,那么利用左表生成的过滤条件就可以把绝大多数在 Join 层要过滤的数据在数据读取时就提前过滤,这样就能大幅度的提升 Join 查询的性能。
当前 Doris 支持三种类型 RuntimeFilter
-
一种是 IN—— IN,很好理解,将一个 hashset 下推到数据扫描节点。
-
第二种就是 BloomFilter,就是利用哈希表的数据构造一个 BloomFilter,然后把这个 BloomFilter 下推到查询数据的扫描节点。。
-
最后一种就是 MinMax,就是个 Range 范围,通过右表数据确定 Range 范围之后,下推给数据扫描节点
RuntimeFilter 类型
Runtime Filter 适用的场景有两个要求:
-
第一个要求就是左表大右表小,因为构建 Runtime Filter是需要承担计算成本的,包括一些内存的开销。
-
第二个要求就是左右表 Join 出来的结果很少,说明这个 Join 可以过滤掉左表的绝大部分数据。
当符合上面两个条件的情况下,开启 Runtime Filter 就能收获比较好的效果。
当 Join 列为左表的 Key 列时,RuntimeFilter 会下推到存储引擎。Doris 本身支持延迟物化,延迟物化简单来说是这样的:假如需要扫描 ABC 三列,在 A 列上有一个过滤条件: A 等于 2,要扫描 100 行的话,可以先把 A 列的 100 行扫描出来,再通过 A = 2 这个过滤条件过滤。之后通过过滤完成后的结果,再去读取 BC 列,这样就能极大的降低数据的读取 IO。所以说 Runtime Filter 如果在 Key 列上生成,同时利用 Doris 本身的延迟物化来进一步提升查询的性能。