OLAP 并发执行架构与调度之 Spark SQL 和 Presto

在大数据处理和分析中,Apache SparkPresto 是在数仓行业中标杆产品,都经常被用于各家数仓架构中用于离线实时建设。尽管这几年云原生大数据数据库产品大爆发,但是从技术架构上说仍然都还是这两种范式。

在这篇文章中,我们首先会用一个简单的SQL说明数据库 Volcano算法,然后再阐述 Spark SQLPresto 分布式执行架构和并发环境下是如何做任务调度的。希望通过这个案例分析能让大家对 StageByStageMPP 执行引擎的区别有所认知,也更希望本文能够帮助大家在数仓选型和业务调优上有一些帮助。

基本算法

首先让我们考虑一个简单的数据库查询,我们将使用伪代码来描述每种方法的处理方式。

ini 复制代码
SELECT *
FROM Orders
JOIN Customers ON Orders.CustomerID = Customers.ID
WHERE Customers.Country = 'Germany';

方案 1

一种简单的方法,我们一次性处理每个操作,并将中间结果存储在某个地方以供下一步使用,这里假设Customers和Orders就是个在内存中的一个集合(在具体业界中,这里涉及的东西非常负载,需要决策怎么去做IO,扫描文件还是索引,怎么处理不同文件,怎么做并发并行)

ini 复制代码
# 假设我们有两个函数: join_tables 和 filter_rows
​
# 首先,我们执行连接操作,并存储结果
joined_result = join_tables(Orders, Customers, on='CustomerID=ID')
​
# 接下来,我们对连接后的结果应用过滤条件,并再次存储结果
final_result = filter_rows(joined_result, where="Country='Germany'")
​
# 最后,我们遍历最终结果并返回
for row in final_result:
    print(row)

在这种方式中,join_tablesfilter_rows 函数分别处理它们的任务并产生中间结果。这些结果可能很大,需要被完整地存储在内存或临时存储中,直到下一步操作完成, 资源消耗就很高。这种实现的好处是控制逻辑很简单,也很容易确定哪个阶段失败了,恢复逻辑简单。

方案 2 (Volcano 算法)

我们可以使用迭代器模式来处理这个查询操作,我们先给每个运算定义好三个接口,open(),next()close() 如下:

python 复制代码
class JoinOperator:
    def __init__(self, left_input, right_input, condition):
        self.left_input = left_input
        self.right_input = right_input
        self.condition = condition
        self.current_left_row = None
​
    def open(self):
        self.left_input.open()
        self.right_input.open()
​
    def next(self):
        while True:
            if self.current_left_row is None:
                self.current_left_row = self.left_input.next()
                if self.current_left_row is None:
                    return None
                self.right_input.rewind()
​
            right_row = self.right_input.next()
            if right_row is not None and self.condition(self.current_left_row, right_row):
                return self.current_left_row, right_row
            elif right_row is None:
                self.current_left_row = None
​
    def close(self):
        self.left_input.close()
        self.right_input.close()
​
class FilterOperator:
    def __init__(self, input_operator, condition):
        self.input_operator = input_operator
        self.condition = condition
​
    def open(self):
        self.input_operator.open()
​
    def next(self):
        while True:
            row = self.input_operator.next()
            if row is None or self.condition(row):
                return row
​
    def close(self):
        self.input_operator.close()

真正执行的时候,迭代器模式就是先创建好迭代器其实就是定义好这个依赖关系。然后从Root节点开始调用 next() 方法时,JoinOperatorFilterOperator 都仅处理并返回一行(部分)结果,而不是整个中间结果集。

python 复制代码
# 创建迭代器
join_op = JoinOperator(OrdersIterator(), CustomersIterator(), condition=lambda o, c: o.CustomerID == c.ID)
filter_op = FilterOperator(join_op, condition=lambda row: row.Customers.Country == 'Germany')
filter_op.open()
​
# 执行查询
while True:
    result_row = filter_op.next()
    if result_row is None:
        break
    print(result_row)
    
filter_op.close()

这种方式其实就是20世纪90年代早期由Goetz Graefe 提出用于数据库查询执行的Volcano 模型。Volcano 模型避免了需要大量内存来存储中间结果,并且可以在数据流经操作符时即时进行处理。这种按需处理的方式使得 Volcano 模型在处理大型数据集时更高效,尤其是在数据无法完全放入内存的情况下,但是会有大量的虚函数调用开销以及某些场景下 cpu cache 命中率不够高。

Spark

架构

上述SQL优化之后会变成下述的Plan,会把过滤条件下推到 JOIN 操作之前 (图1(a))。然后,SparkDAG执行引擎将这个优化的计算计划转换成一个或多个作业, 每个作业由一个DAG的Stage组成,图1(b)表示产生当前作业最终结果的数据变换的血缘关系。每个Stage之间的中间数据通过 shuffle操作传输。

Shuffle 算法

Shuffle操作是 MapReduce 计算范式的关键所在,它将涉及的中间数据 mapreduce任务之间之间进行传输。尽管 shuffle 操作的基础概念很直接,但是不同框架采取了不同的方法来实现它。

在Spark中,根据部署模式的不同,执行shuffle的方式也略有不同。在YARN上部署Spark,并利用外部shuffle服务来管理shuffle数据的部署模式下,Spark中的shuffle操作工作如图2所示:

  1. 每个Spark执行器启动时都会向位于同一节点上的Spark外部shuffle服务(ESS)注册。这样的注册让Spark ESS知道每个已注册执行器的本地map任务产生的实体化shuffle数据的位置。注意,Spark ESS实例是独立于Spark执行器的,并且可能在许多Spark应用程序之间共享。

  2. shuffle map 阶段中的每个任务处理它的数据部分。在map任务结束时,它产生一对文件,一个用于shuffle数据,另一个用于索引前者中的shuffle块。为了做到这一点,map任务根据分区键的哈希值对所有转换后的记录进行排序。在这个过程中,如果map任务不能在内存中对所有数据进行排序,它可能会将中间数据溢出到磁盘上。排序完成后,就会生成shuffle数据文件,在该文件中,属于同一shuffle分区的所有记录被组织到一个shuffle块中。同时也会生成相应的shuffle索引文件,该文件记录了块边界偏移量。

  3. 当下一阶段的reduce任务开始运行时,它们将向Spark驱动程序查询它们输入shuffle块的位置。一旦这个信息可用,每个reduce任务将建立与相应的Spark ESS实例的连接,以便获取它们的输入数据。Spark ESS在收到这样的请求后,利用shuffle索引文件跳转到shuffle数据文件中相应的块数据,从磁盘上读取它,并将其发送回reduce任务。

DAG Task 调度

经过Stage划分之后,会产生一个或者多个互相关联的Stage。其中,真正执行Action算子的RDD所在的Stage被称为Final StageDAGScheduler会从这个Final Stage生成作业实例。

在提交Stage时,DAGScheduler会先判断该Stage父Stage的执行结果是否可用。如果所有父Stage的执行结果都可用,则提交该Stage。如果有任意一个父Stage的结果不可用,则尝试递归提交该父Stage

所以,从上述两个角度来看,就可以发现 Spark这种引擎实现的时候是通过Shuffle把数据持久化,一个Stage一个Stage的调度执行,我猜这也是业界说这种方式是StageByStage的原因。

Presto / Trino

Task 调度

还是上面的SQL 经过Trino Parse/Optimize 之后会变成下述的Plan,然后按照 Stage 将这个 Plan 转换成一个或多个 Task,每个Stage之间的中间数据通过 shuffle操作传输。

这里和 Spark 不同的地方在于:

i. shuffle 不落地,只是把数据放到内存中。比如Stage 1-2 中的buffer,按照下游并发的数量分桶,Task 中PartitionOutput 算子会把数据按照shuffle 的规则分到这几个buffer中,下游Stage 3 每个Woeker(并发)分来来拉取对应buffer中的数据。

ii. 在某个Query内,下游的Stage 也不需要等待前面的Stage的所有Task都计算完才开始拉数据,而是会定期轮询找上游依赖的Stage 来拉取部分buffer数据。不过下图中的Join(HashJoin的话)比较特殊,会先把一边的数据全部拉齐,建好HashTable,然后再开始pipeline拉取另一边的数据,往后pipeline起来。

本地数据流

如下图所示,在 Trino 执行模型中,除了节点的并发之外,还有节点内部的并行(实现过程中是线程上的并行)。一个Stage会生成多个Task调度到到各个 Worker节点 内执行,每个Task内还会分成多个pipeline(一般是一个),每个pipeline内部会并发生成多个Driver,每个Driver执行的内容都是一段一样的算子树。

每个Driver处理的数据单元被抽象成Split,被指派给一个线程执行。Trino的DriverLoop比流行的Volcano模型(拉模式)的递归迭代器要复杂,首先它更加适合协作式多任务处理,因为在让出线程之前,可以快速将操作符带到一个已知的状态,而不是无限期地阻塞。此外,Driver可以在没有额外输入的情况下在操作符之间移动数据(例如,恢复资源密集型或爆炸性转换的计算)来最大限度地增加每一个时间段执行的工作。

DriverLoop持续地在操作符之间移动Page,直到调度时间片完成,或直到操作符无法取得进展。详细的逻辑大家可以参考代码io.trino.operator.Driver#processInternal()

Stage之间的调度

Trino旨在最大限度地减少端到端延迟,同时最大化资源利用率。Trino使用基于内存的buffer,通过HTTP来交换中间结果, 任务产生的数据存储在内存buffer中,供其他Worker消费。使用HTTP长轮询从其他工作节点请求中间结果,这种机制提供的延迟比持久化到磁盘的其他系统要低得多。

从上面看的出来,Trino是以 pipeline 方式保证数据持续流动的。在某个Task内不需要前面的Operator计算完所有数据再输出结果给后面的Operator,在某个Query内也不需要前面的Stage的所有Task都计算完所有数据再输出结果给后面的Stage。

相关推荐
Data-Miner13 分钟前
196页满分PPT | 集团流程优化及IT规划项目案例
大数据·数据分析
徐*红16 分钟前
Elasticsearch 8.+ 版本查询方式
大数据·elasticsearch
DolphinScheduler社区28 分钟前
怎么办?用DolphinScheduler调度执行复杂的HiveSQL时无法正确识别符号
大数据
goTsHgo30 分钟前
Hive自定义函数——简单使用
大数据·hive·hadoop
码爸33 分钟前
flink 例子(scala)
大数据·elasticsearch·flink·scala
FLGB33 分钟前
Flink 与 Kubernetes (K8s)、YARN 和 Mesos集成对比
大数据·flink·kubernetes
码爸35 分钟前
flink 批量压缩redis集群 sink
大数据·redis·flink
core51235 分钟前
Flink官方文档
大数据·flink·文档·官方
周全全38 分钟前
Flink1.18.1 Standalone模式集群搭建
大数据·flink·集群·主从·standalone
Hello.Reader41 分钟前
StarRocks实时分析数据库的基础与应用
大数据·数据库