上文我们讲过了常用的转换算子,本篇博客记录一下常用的行动算子,那我们开始啦。
一、Action算子和transformations算子的区别
- transformations算子是惰性算子,也就是说它的触发条件不是代码执行到这,而是需要transformations算子的结果后才执行这个算子,transformations也为我们减少了很多非必要的开销。
- Action算子是一个触发计算的算子,我们知道Spark是由很多的任务组成,而一个Action便形成了一个新的任务,也及时Task。可能有一个或者多个Task组成了我们的application。
既然都讲到这了,我们顺便讲一讲Spark的任务组成吧。前面说了可能会造成shuffle的算子,这里我们成为shuffle算子,切记shuffle算子和transformations算子、Action算子并不是一回事。每一个shuffle都将是一个stage,一个Task可能有一个或者多个stage组成,然后一个或者多个Task组成了我们的应用。
这个是一个task的DAG(有向无环图),然后如下图,多个stage组成Task,很多的Task组成一个application。我们知道stage的一般形成条件是出现shuffle算子,而Task的形成条件是Action算子,我们可以随便打开一个行动算子源码,可以看到里面均有一行大致相同的代码,sc.runJob就是触发计算的触发器,当调用sc.runJob后才开始真正的计算。
sc.runJob(this, reducePartition, mergeResult)
二、Action算子
1、reduce
reduce是Spark行动算子里的聚合算子,主要作用是对RDD做聚合作用,比如求和,极值之类的。相对也比较简单。那我们先看一下使用方法:
// TODO 创建执行环境
val conf = new SparkConf().setMaster("local[*]").setAppName("create")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 4, 5, 6, 7, 8, 9))
// 求和
val i: Int = rdd1.reduce(_ + _) // output: 42
// 求最大值
val i1: Int = rdd1.reduce((x, y) => {
if (x > y) {
x
}
else
y
}) // output: 9
用法就是这样简单,要是想求平均的话,直接在和的基础上在除以元素个数即可。
那我们在看一下源码:
def reduce(f: (T, T) => T): T = {
val cleanF = sc.clean(f)
val reducePartition: Iterator[T] => Option[T] = iter => {
if (iter.hasNext) {
Some(iter.reduceLeft(cleanF)) //一个一个迭代,从左开始
} else {
None
}
}
var jobResult: Option[T] = None
val mergeResult = (index: Int, taskResult: Option[T]) => {
//当每个任务执行完后,都会返回一个这样的结果,index为分区的索引
if (taskResult.isDefined) {
jobResult = jobResult match {
case Some(value) => Some(f(value, taskResult.get)) //第二个分区的结果与第一个分区的一起,再用f处理
case None => taskResult //当第一个任务完成时,会进入,taskResult 为第一个分区的结果
}
}
}
sc.runJob(this, reducePartition, mergeResult) //任务的执行
// Get the final result out of our Option, or throw an exception if the RDD was empty
jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}
通过源码,可以看到reduce先有reducePartition获取一个分区的结果的方法,然后再合并结果的方法,最后将他们一并传给 sc.runJob开始计算。有人疑问,为什么把Action的方法传入 sc.runJob就可以获取结果,前面提到有向无环图,当我们把action的方法给runJob,先去寻找该行动算子计算所需要的数据,接着再寻找这个数据的来源,又有人要问了,那怎么寻找了?看上图有向无环图,在Saprk内部维护了一个有向无环图,当我们需要这个数据时候,我们就开始一级一级的往上寻找,直至找到已存在的数据源,然后再往回一级一级的计算,直到获取最后的Action结果,有点绕,慢慢思考一下就可以了。
2、count
count算子就是获取RDD中元素个数,用法简单,先看一下用法:
val conf = new SparkConf().setMaster("local[*]").setAppName("create")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 4, 5, 6, 7, 8, 9))
val rdd: RDD[(String, Double)] = sc.makeRDD(List(("Tom", 88.0), ("Tom", 90.0), ("jack", 86.0), ("Toni", 85.0), ("jack", 82.0), ("Tom", 93.0)),2)
val l: Long = rdd1.count() // 8
源码:
/**
* Return the number of elements in the RDD.
*/
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
3、collect
collect算子就是将RDD元素以scala的数据结构Array形式展示,可以直接打印:
val conf = new SparkConf().setMaster("local[*]").setAppName("create")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc