一、引入
假设有如下示例代码,按照我们之前分析的逻辑执行计划,它将如图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)
在本章中,我们要解决的问题是如何将逻辑处理流程转成物理执行流程。
二、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()操作的。
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的计算。