通过上节内容我们知道一个Application包括多个JOB,那么JOB划分是代码中上一个Action操作,会划分一个JOB,就是说多个Action操作就会有多个JOB,JOB执行的顺序是从第一个开始。我们上一节分析源码讲到最终提交一个JOB的执行是调用了dagScheduler.runJob方法执行,本章节就接着一上节内容,详细剖析DAGSchdeuler中如何在JOB中划分Stage的。我们就以wordCount为例来剖析:
val lines = sc.textFile(...)
val words = lines.flatMap(line => line.split(" "))
val pairs = words.map(word => (word, 1))
val counts = pairs.reduceByKey(_ + _)
counts.foreach(count => println(count._1 + ": " + count._2))
val lines = sc.textFile(...)
对于这一句代码,我们知道首先是生成一个HadoopRDD,HadoopRDD会调用map方法转代换成一个MappedRDD.
val words = lines.flatMap(line => line.split(" "))
实际上这个会把MappedRDD通过flatMap算子,转换成一个FlatMappedRDD
val pairs = words.map(word => (word, 1))
这一句代码,我们不用说了吧,调用map当然又一次转换成了MappedRDD,当代码执行到这里时,这时数据格式是什么了,是不是:
(hello,1)
(word,1)
(hello,1)
(me,1)
...
接下来我们是不是要执行reduceByKey操作吗,其实这个时候,我们会使用HashPartitioner,将每个key写入对应的partition的本地磁盘文件中
val counts = pairs.reduceByKey(_ + _)
当执行上面这句时,内部因为发生了Shuffle,就发生了复杂的操作:
1:首先生成一个MapPartitionRDD,其实这个就是我们上面那个partition本地文件的RDD,这时的数据是什么呢,是不是相同的key写到了一个partition文件里了,如下:
(hello,1)(hello,1) ...
(word,1)(word,1) ...
当我们另一个节点如果也有相同的partition文件对应的task,是不是会把各个节点中的task中对应的partition文件聚合到一起。
2:这样将所以节点上的partition文件聚合在一起时(通过网络传输,这就是Shuffle操作),就生成一个ShuffleRDD,文件格式如下:
(hello,1)(hello,1)(hello,1)(hello,1)...
(word,1)(word,1) (word,1)(word,1) ...
这里的数据一定比步骤1的多,因为这是从各个节点聚合来的.
这个ShuffleRDD一种中间性质的RDD,前面MapPartitionRDD与生成的ShuffleRDD之间的依赖是宽依赖。这个一定要明白。
3:对每个partition的数据通过key聚会,就又生成了一个新的MapPartitionRDD,聚合的一的数据:
(hello,3)
(word,7)
通过上面三步的操作这个rdeuceByKey就执行完毕了。
整个过程是不是只有一个宽依赖操作,不是上面的步骤2.其实DAGScheduer在提交JOB时,对这些都已经掌握,划分stage的原理就是通过宽依赖来分的。
DAGScheduler的stage的划分算法:
会从触发action操作的那个RDD生成一个stage,倒推这个RDD的上一步RDD是窄依赖,就将这个RDD划分到这个stage,如果发现对基本个RDD是宽依赖,那么就将这个宽依赖的RDD创建一个新的stage,那么这个RDD就是新的stage的最后一个RDD,然后以此类推,根据窄依赖或者宽依赖进行stage的划分 ,直到所有的RDD都遍历为止。