Spark源码分析之 — Job触发流程原理与源码分析

为了能够很好的掌握Job是如何触发的,我们以一个WordCount的程序示例来进行分析,简单的Scala的代码如下(这里可以对着最新的Spark源码来看):

// 初始化SparkConf,在本地运行
val conf = new SparkConf()                          (1)  
      .setAppName("WordCount")
      .setMaster("local")
	  
// 初始化SparkContext
val sc = new SparkContext(conf)                     (2)
	
// 读取spark.txt里面的内容,设置partition的数量为1
val lines = sc.textFile("spark.txt", 1)             (3)
	
// 将读取出来的单词,按照空格进行切分
val words = lines.flatMap{line => line.split(" ")}  (4)
	
// 将每个单词映射成(key, value) => (key, 1)的形式
val pairs = words.map{ word => (word, 1)}           (5)
	
// 将相同的key出现的次数进行累加
val wordCounts = pairs.reduceByKey{ _+_ }           (6)
	
// 打印出每个单词出现的次数
wordCounts.foreach(wordCount => println(wordCount._1 + " appeared " + wordCount._2 + " times."))     (7)

如上所示:

1、第一行是初始化配置文件,SparkConf,设置运行的任务名称和在本地运行Spark程序;

2、第二行:就是初始化SparkContext,这里面比较重要的是,创建了两个组件:DAGScheduler和TaskScheduler(TaskSchedulerImpl),当然也包括SparkUI组件,这里就不描述了;

3、第三行:从本地文件"spark.txt"中读取数据,并且设置partition的数量是1,通过查看源码发现,读取文件使用了Hadoop读取文件的方式,它会生成一个中间的RDD - HadoopRDD,它对应的 (key, value) =>  (每一行的偏移量offset,  每一行的文本值),我们取出每一行的文本值,就是我们需要的RDD,也就是MapPartitionsRDD。具体的源码如下:

4、第四行:使用flatMap,将linesRDD按照空格进行切分,也就是过滤掉空格,源码如下:

5、第五行:对过滤后的单词进行映射,映射成(word, 1)的形式,使用map操作,源码如下:

6、第六行:将wordsRDD中每个相同单词的key进行累加。这里注意,在源码文件RDD.scala中是没有reduceByKey()方法的,因此对RDD调用reduceByKey()方法的时候,会触发scala的隐式转换;此时就会在作用域里找隐式转换,这里的隐式转换是:rddToPairRDDFunctions()方法,然后将RDD转换为PairRDDFunctions,接着会调用PairRDDFunctions中的reduceByKey()方法。源码如下:

7、通过观看源码发现,reduceByKey()方法是通过combineByKey()来实现的,我们进入combineByKey()分析一下:

8、先看这个函数的传入参数是什么:

如上所示,其中:

createCombiner: V => C用于将输入类型RDD[K, V]中的V转化为输出参数RDD[K, C]中的C。它在每个分区上都会执行,只要遇到没有处理过的key,就会执行该方法。

mergeValue: (C, V) => C:合并函数,将输入类型C,和类型V的值进行合并,得到类型C,输入参数是(C, V),输出是 C。这个方法在每个分区上都会执行,和createCombiner不同的是,它只处理在本分区内已经处理过的key。在遍历RDD的时候,如果遇到的key不是第一次出现,那么就是将V累加到聚合对象C中。

mergeCombiners: (C, C) => C:因为combineByKey是在分布式环境下执行的,RDD的每个分区单独进行combineByKey操作,最后需要将各个分区的结果进行聚合,这个函数就是聚合各个分区的结果,得到最终的结果。

partition: Partitioner : 分区函数,默认是HashPartitioner

mapSideCombine: Boolean = true: 用于判断是否需要在map端进行combine操作,类似于MapReduce中的combine,默认是true。

serializer:Serializer = null:默认不进行序列化。

9、下面具体分析combineByKey的源码:

首先先判断key是否是数组,如果是数组的话,就不能使用map端的合并和HashPatitioner了,原因,要想进行Map端合并和Hash分区,那么key必须能通过比较内容判断是否相等,以及通过内容计算hash值,进而进行合并和分区,然后数组判断相等和计算hash并不是依据它里面的内容的。

接着定义和combineByKey的核心,Aggregator,如下所示:

传入了我们定义好的三个自定义函数,这里简单进行分析,具体的等到Spark Shuffle的时候进行详细分析:

实例化Aggregator之后,就判断,是否需要重写分区(shuffle),也就是下面的if判断:

如果不需要分区,对于咱们这个WordCount示例而言,是不需要进行分区的,因为partition设置的是1,因此直接调用aggregator的combineValuesByKey()方法,对数据进行合并,这个方法里面根据是否刷磁盘分为两路,两路做的事情是一样的,区别在于,当一个内存不够的时候就OOM,而另外一个是可以刷磁盘。

如果需要分区,需要将现在分区的key进行重新分区,目的在于将相同的key汇集到同一个分区上,由于数据分布不确定性,因此有可能现在的每个分区的数据是由重新分区后的所有分区的部分数据组成(宽依赖),因此需要进行shuffle,因此构建了ShuffledRDD。

这里,第六行结束以后,我们就能得到按照每个key,进行聚合的(key,value)形式的RDD。

10、第七步:使用foreach方法打印,foreach()方法是action操作,注意,这里触发了job,我们进入源码看看:

这里可以看出,在spark中,只有action操作,才会触发Job任务,前面的transformation操作是具有Lazy特性的。通过查看代码,我们发现,runJob()方法是通过调用SparkContext,已经初始化好的DAGScheduler的runJob()方法,源码如下:

以上就是Job触发流程,最终调用的是DAGScheduler的runJob()方法,实际上,一个action触发一个Job任务。下一篇我们具体分析一下,DAGScheduler的stage划分算法。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值