Stage划分的时候,大家应该都知道是从最后一个stage向根据宽窄依赖,递归进行stage划分。
但是代码里面涉及的逻辑复杂。毕竟涉及到相互递归调用。让人似懂非懂。 反正我是炸毛了 o(╥﹏╥)o
本文专门用一篇文章详细论述DAGScheduler 的 stage 划分流程
为了更容易理解,本文采用 debug模式+实例+源码的方式进行讲解
首先写一个WordCount代码(这个代码,为了观察多个suffle操作,我写了两个reducebykey 函数)
源代码:
直接执行代码,查看spark执行程序时,将代码划分stage生成的DAG流程图
可知: WordCount 在stage划分的时候,划分为三个stage
即在代码中如下标识:
首先,我们明确一个概念RDD
我们知道RDD有两个重要属性 id , name
为了在后面调试的时候,清除的理解rdd之间的调用,需要对其做编号,本文以rdd的id进行区分
声明的rdd的属性我举几个例子:
zrdd1 :
类型: MapPartitionsRDD
id : 1
name : /tmp/zl/data/data.txt (注:只有zrdd1的name有值,为数据路径, 其他的rddname值都是“null ” )
zrdd4:
类型: ShuffledRDD
id : 4
name : null
直接说结果:
属性 | RDD Id (重要,区分标识) | RDD类型 |
zrdd1 | 1 | MapPartitionsRDD |
zrdd2 | 2 | MapPartitionsRDD |
zrdd3 | 3 | MapPartitionsRDD |
zrdd4 | 4 | ShuffledRDD |
zrdd5 | 5 | MapPartitionsRDD |
zrdd6 | 6 | ShuffledRDD |
zrdd7 | 7 | MapPartitionsRDD |
程序入口的触发点即为: zrdd7.count() 方法。 实际执行的是runjob方法。开启程序执行入口。
程序依赖关系如下图:
接下来我们看源码解析代码查看stage是如何划分的:
即如下代码调度流程图中标识的部分。
以为之前的文章有说明,所以不再详细解释。有兴趣的小伙伴可以直接看
好了,我们开始正式说代码:
org.apache.spark.scheduler.DAGScheduler#createResultStage
这个方法里面最重要的是getOrCreateParentStages 方法,从这就容易开始乱了。
别慌,我先给画个调用图,先搞清楚逻辑,再用debug跟一便就好了。
从图上可知,最外层循环的主体为: getOrCreateParentStages
记住这个啊。 这个才是真正的循环调用创建stage的方法,不要被getShuffleDependencies这个方法所迷惑
getShuffleDependencies 这个方法只是根据一个rdd返回这个rdd所在的宽依赖 ShuffleDependency
好了,先看一下类中的代码,然后我在画个图,讲解
getOrCreateParentStages:
根据给定的RDD获取或者创建父stages列表 ,新的stage会根据提供的firstJobId进行创建
这个方法很重要,递归调用的就是这个方法:
getShuffleDependencies
根据给定的RDD获取或者创建父stages列表
返回值结构: ShuffleDependency 是一个宽依赖
getOrCreateShuffleMapStage (这个方法注意看一下)
getMissingAncestorShuffleDependencies
这里面有一个递归方法 getShuffleDependencies 获取shuffle依赖 (缓存过的即为处理过的,不做任何处理)
ArrayStack 栈是一种后进先出(LIFO)的数据结构。
所以在循环的时候,最先取出的值,是最后放进的值。
createShuffleMapStage
根据所给的 ShuffleDependency 创建 ShuffleMapStage
这个里面尤其要注意一点:
val parents = getOrCreateParentStages(rdd, jobId)
好了,接下来,我们画个图理解一下。
其实也不用画图。
主要是:
val deps = getMissingAncestorShuffleDependencies(shuffleDep.rdd)
这句,直接会吧所有宽依赖的都会找出来,然后提交。
返回的数据结构是 ArrayStack 这个数据结构是栈是一种后进先出(LIFO)的数据结构
用递归的的方式拿到stage ,然后再取出
因为存储的时候,是栈存储,所以提交的时候是stage0, 带入上面的方法:
val parents = getOrCreateParentStages(rdd, jobId)
stage0没有parents,所以返回值,为空。然后将stage0加入缓存。 如下代码
stageIdToStage(id) = stageshuffleIdToMapStage(shuffleDep.shuffleId) = stage
当在传入stage1的时候,获取父的依赖,也就是stage0,这个在上一次调用的时候,已经处理过了
已经获取到了,所以在调用getOrCreateParentStages方法的时候,可以直接从缓存中拿到值。
如下方法,直接从缓存中获取。相当于做了一个优化。
好了,下面是画图的方式说了一下,有不明白的地方可以给我留言。
举例:
根据代码划分:stage的时候是这个结构:
入栈:
出栈:
好了,接下来看一下图多个依赖,提交的时候。流程图
多个依赖提交例子 (深度遍历算法)
RDDs原始依赖图
getShuffleDependencies
RDD:15 , 获取上一层依赖,返回的结果是 ShuffleDependency 集合
getMissingAncestorShuffleDependencies
深度遍历顺序获取所有祖先的宽依赖,这里返回的是一个集合。 其实这个也是一个优化,如果采用递归方法的调用的话,
很容易因为嵌套层级过多,导致栈溢出。
传入值如果是RDD:13 返回 红色的宽依赖。
最后划分结果