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。

相关推荐
只因只因爆28 分钟前
spark小任务
大数据·分布式·spark
cainiao08060534 分钟前
Java 大视界——Java 大数据在智慧交通智能停车诱导系统中的数据融合与实时更新
java·大数据·开发语言
End9283 小时前
Spark之搭建Yarn模式
大数据·分布式·spark
我爱写代码?3 小时前
Spark 集群配置、启动与监控指南
大数据·开发语言·jvm·spark·mapreduce
TDengine (老段)4 小时前
什么是物联网 IoT 平台?
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
青云交4 小时前
Java 大视界 -- 基于 Java 的大数据分布式存储在工业互联网海量设备数据长期存储中的应用优化(248)
java·大数据·工业互联网·分布式存储·冷热数据管理·hbase 优化·kudu 应用
艾醒(AiXing-w)4 小时前
探索大语言模型(LLM):国产大模型DeepSeek vs Qwen,谁才是AI模型的未来?
大数据·人工智能·语言模型
£菜鸟也有梦5 小时前
从0到1上手Kafka:开启分布式消息处理之旅
大数据·kafka·消息队列
Elastic 中国社区官方博客5 小时前
在 Elasticsearch 中删除文档中的某个字段
大数据·数据库·elasticsearch·搜索引擎
时序数据说5 小时前
时序数据库IoTDB分布式系统监控基础概述
大数据·数据库·database·时序数据库·iotdb