Spark物理执行计划

一、引入

假设有如下示例代码,按照我们之前分析的逻辑执行计划,它将如图4.1所示的逻辑处理流程。

val data1 = Array[(Int, Char)] ((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), (3, 'f'), (2, 'g'), (1, 'h'))
val rdd1 = sc.parallelize(data1, 3)
// 使用HashPartitioner对rdd1重新划分
val partitionedRDD = rdd1.partitionBy(new HashPartitioner(3))

//构建一个<K, V>类型的rdd2,并对rdd2中的Value进行复制
val data2 = Array[(Int, String)]((1, "A"), (2, "B"), (3, "C"), (4, "D"))
val rdd2 = sc.parallelize(data2, 2).map(x => (x._1, x._2 + "" + x._2))

//构建一个<K, V>类型的rdd3
val data3 = Array[(Int, String)]((3, "X"), (5, "Y"), (3, "Z"), (4, "Y"))
val rdd3 = sc.parallelize(data3, 2)

//将rdd2和rdd3进行union()操作
val unionedRDD = rdd2.union(rdd3)
//将被重新划分过的rdd1和unionedRDD进行join()操作
val resultRDD = partitionedRDD.join(unionedRDD)

//输出join()操作后的结果,包括每个record及其index
resultRDD.foreach(println)

image-20230919094359405

在本章中,我们要解决的问题是如何将逻辑处理流程转成物理执行流程。

二、Spark物理执行计划生成方法

Spark具体采用3个步骤来生成物理执行计划:(1) 首先根据action() 操作顺序将应用划分为job;(2)然后根据每个job逻辑处理流程中的ShuffleDependency依赖关系,将job划分为stage;(3)最后在每个stage中,根据生成RDD的分区个数生成多个task。具体如下所述。

2.1 根据action()操作顺序将应用划分成job

这一步主要解决何时生成job,以及如何生成job逻辑处理流程的问题。当应用程序出现action()操作时,如resultRDD.action(),表示应用会生成一个job,该job的逻辑处理流程为从输入数据到resultRDD的逻辑处理流程。例如,在第一章的示例代码,我们在join()之后使用了foreach()这一action()操作,因此,会生成出一个如图4.3的物理执行流程。如果应用中有很多action()操作,那么Spark会按照顺序为每个action()操作生成一个job,每个job的处理流程也都是从输入数据到最后action()操作的。

image-20230919201210522

2.2 根据ShuffleDependency依赖关系将job划分为stage

对于每个job,从其最后的RDD(图4.3中连接results的MapPartitionsRDD)往前回溯整个逻辑处理流程,如果遇到NarrowDependency,则将当前RDD的parent RDD纳入,并继续往前回溯。当遇到ShuffleDependency时,停止回溯,将当前已经纳入的所有RDD按照其依赖关系建立一个stage,命名为stage i。(从后往前回溯的原因是有可能子RDD会依赖多个父RDD,如果从前往后回溯,拿到的DAG就不正确)。

如图4.3所示,首先从results之前的MapPartitionsRDD开始向前回溯,回溯到CoGroupedRDD时,发现其包含两个parent RDD,其中一个是UnionRDD。因为CoGroupedRDD与UnionRDD的依赖关系是ShuffleDependency,对其进行划分,并继续从CoGroupedRDD的另一个parent RDD回溯,回溯到ShuffledRDD时(join操作被翻译成了ShuffledRDD和CoGroupedRDD),同样发现了ShuffleDependency,对其进行划分得到了一个stage2。接着从stage2之前的UnionRDD开始向前回溯,由于都是NarrowDependency,将一直回溯到读取输入数据的RDD2和RDD3中,形成stage1。最后,只剩余RDD1成为一个stage0。

2.3 根据分区计算各个stage划分成task

执行第2步后,我们可以发现整个job被分成了逻辑分明的执行阶段stage。接下来的问题是如何生成计算任务。因为每个分区上的计算逻辑都相同,而且都是独立的,因此每个分区上的计算可以独立成为一个task。根据每个stage中最后一个RDD的分区个数决定生成task的个数。

如在图4.3的stage2中,最后一个MapPartitionsRDD的分区个数为3,那么stage2就生成3个task。如图4.3中粗箭头所示,在stage2中,每个task负责ShuffledRDD => CoGroupedRDD => MapPartitionsRDD => MapPartitionsRDD中一个分区的计算。

同样,在stage1中生成4个task,前2个task负责 Data blocks => RDD2 => MapPartitionsRDD => UnionRDD中2个分区的计算,后2个task负责Data blocks => RDD3 => UnionRDD中2个分区的计算。

在stage0中,生成3个task,负责Data blocks => RDD1的计算。

Spark 中,执行计划是指一系列的逻辑和物理转换,将 Spark 代码转换为可以在集群上执行的任务。Spark执行计划中使用了许多优化技术,包括投影和过滤操作的下推、左外连接的优化、广播变量的优化等等。Spark执行计划主要分为以下两个阶段: 1. 逻辑执行计划Spark 将用户代码转换为一系列的逻辑操作,这些操作构成了逻辑执行计划。逻辑执行计划是基于 RDD 抽象的,它描述了 RDD 之间的依赖关系和转换操作。 2. 物理执行计划Spark 将逻辑执行计划转换为一系列的物理操作,这些操作构成了物理执行计划物理执行计划是基于具体的执行引擎的,它描述了如何将逻辑操作映射到实际的节点和任务上。 在执行计划的生成过程中,Spark 使用了许多优化技术,包括: 1. 延迟计算:Spark 采用了延迟计算的策略,即只有在需要计算结果时才会触发计算操作。这种策略可以避免不必要的计算,提高计算效率。 2. 任务划分:Spark 将大的数据集划分成小的分区,每个分区分配一个任务进行处理。这种策略可以实现并行计算,提高计算效率。 3. 数据共享:Spark 可以使用广播变量和累加器等机制实现数据共享,避免重复计算,提高计算效率。 4. 优化器:Spark 使用了一个优化器来对执行计划进行优化,包括选择最优的执行计划、下推操作等。 5. 缓存机制:Spark 可以使用缓存机制来避免重复计算,提高计算效率。 总之,Spark执行计划是一个非常重要的概念,它决定了 Spark 代码在集群上的执行方式和效率。Spark执行计划采用了许多优化技术,可以帮助用户快速、高效地处理大规模数据集。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值