文章目录
- 前言
- 一 累加器的作用
- 二 自定义累加器
- 总结
前言
spark中有三大数据模型RDD、累加器以及广播变量,其中RDD是重中之重,所以后面我会出一系列专门讲解RDD的文章,今天我们说的也是比较重要的累加器
一 累加器的作用
累加器:又叫分布式共享只写变量
可能现在还不是很理解这是什么意思,那么讲解累加器的作用之前我们先来看一个需求:
我们需要将一个集合中的数据求和,我们可以这样求解(下面所有操作都是在idea代码实现的):
val rdd = sc.makeRDD(List(1,2,3,4))
val i = rdd.reduce(_+_)
println(i)
这样我们完全能够求出最后的结果,但是reduce方法是需要将数据打乱重组的,肯定会进行shuffle操作,这样数据量较大的时候会非常消耗服务器性能的
我们又想如果可以定义一个变量用来存储结果,遍历集合进行累加就行了,因为遍历是不用进行shuffle的
接下来的代码:
var sum = 0
val rdd = sc.makeRDD(List(1,2,3,4))
rdd.foreach(num => {
sum += num
})
println(sum)
满心欢喜的运行后结果却不是我们预想的那样,结果为0
我们在foreach里面打印sum的值却可以看到sum是累加过num的,仔细思考发现因为scala闭包的原因,我们在函数里面调用外部的变量会在其内部也创建相同的变量,所以sum能够传到每个executor执行结果,但是经过计算的sum却不能返回给driver,所以打印的还是我们初始定义的为0的变量sum,如下图:
所以这时候我们就能用到累加器了,累加器就是为了解决这个问题的,对分布式集群,值是可见的。
具体运用代码:
//声明累加器,分布式共享只写变量
val sum = sc.longAccumulator
val rdd = sc.makeRDD(List(1,2,3,4))
rdd.foreach(num => {
//调用累加器
sum.add(num)
})
println(sum.value)
简单的运用之后发现,对于数值的累加或者数据的累加,累加器有着非常好的性能以及简洁方便的使用,所以对于以后关于这类的需求应尽量使用累加器来完成
二 自定义累加器
可是我们发现,spark自带的累加器有时候并不能完成我们业务的需求,这点开发人员肯定会考虑到了,让我们可以基于模板自定义符合需求的累加器
比如我们第一次面对的wordCount这个需求,我们可以自定义累加器来完成
代码如下:
object MyAccDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("wc")
val sc = new SparkContext(conf)
val rdd = sc.makeRDD(List("hello spark","hello scala","hello spark","spark scala"))
//声明自定义累加器
var sum = new MyWordCountAcc
//需要注册累加器到spark
sc.register(sum)
//因为没有将数据打乱重组所以不会进行shuffle,提高了性能
rdd.flatMap(_.split(" ")).foreach(
word => {
sum.add(word)
}
)
println(sum.value)
sc.stop()
}
import scala.collection.mutable
//自定义累加器,需要继承spark提供的抽象类,并实现抽象方法
class MyWordCountAcc extends AccumulatorV2[String, mutable.Map[String,Int]]{
var wordMap = mutable.Map[String,Int]()
//判断累加器是否是初始化
override def isZero: Boolean = wordMap.isEmpty
//复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = new MyWordCountAcc
//重置累加器
override def reset(): Unit = wordMap.clear()
//累加逻辑
override def add(word: String): Unit = {
wordMap.update(word,wordMap.getOrElse(word,0)+1)
}
//合并各个executeor传回来的值
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
var map1 = wordMap
var map2 = other.value
wordMap = map1.foldLeft(map2)(
(map,kv) => {
map.update(kv._1,map.getOrElse(kv._1,0)+kv._2)
map
}
)
}
//显示值
override def value: mutable.Map[String, Int] = wordMap
}
}
完成之后,其实我们对这几个方法有点迷糊,对于add、value、merge这三个方法还好,从字面意思我们就知道了,并且也在前面运用累加器的时候用到过
但是isZero、copy、reset这几个方法是什么意思呢?
我们改变isZero方法体的内容试试
override def isZero: Boolean = !wordMap.isEmpty
运行之后报错:
报错的信息是我们传的不是一个空的value,我们追踪源码进去看看
我们可以看到调用这个方法是在序列化一个对象的时候,我们知道调用action算子时会生成一个job,提交job到executor执行,肯定会序列化我们定义的累加器
所以我们追踪foreach方法,一路runjob
其实到这里之后已经是java的方法,我们继续
到此时我们已经能完全理清为什么要有这几个方法了
总结
1)在driver端提交任务时,会序列化rdd需要用到的对象,实现集群之间数据的对象传输,最后会判断该对象是否有writeReplace方法,如果有就调用该对象的writeReplace方法
2)累加器中正好实现了这个方法,所以在传输累加器对象时会调用该方法,并在该方法中调用了累加器对象中的copy、reset和isZero方法,就是每个executor都需要复制一份累加器对象,并将对象中的数据进行reset,最后还会判断数据是否为空,如果不为空则报错
至此,本次分享就结束了,我想大家应该大致了解了为什么要用累加器、累加器的作用以及累加器内部方法的作用了。