文章目录

  • 前言
  • 一 累加器的作用
  • 二 自定义累加器
  • 总结


前言

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,如下图:

spark 累加器原理 spark累加器的作用_spark

所以这时候我们就能用到累加器了,累加器就是为了解决这个问题的,对分布式集群,值是可见的。

具体运用代码:

//声明累加器,分布式共享只写变量
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

运行之后报错:

spark 累加器原理 spark累加器的作用_ide_02

报错的信息是我们传的不是一个空的value,我们追踪源码进去看看

spark 累加器原理 spark累加器的作用_spark_03

spark 累加器原理 spark累加器的作用_ide_04

我们可以看到调用这个方法是在序列化一个对象的时候,我们知道调用action算子时会生成一个job,提交job到executor执行,肯定会序列化我们定义的累加器

所以我们追踪foreach方法,一路runjob

spark 累加器原理 spark累加器的作用_ide_05


spark 累加器原理 spark累加器的作用_scala_06


spark 累加器原理 spark累加器的作用_ide_07


spark 累加器原理 spark累加器的作用_spark_08

其实到这里之后已经是java的方法,我们继续

spark 累加器原理 spark累加器的作用_spark_09

到此时我们已经能完全理清为什么要有这几个方法了

总结

1)在driver端提交任务时,会序列化rdd需要用到的对象,实现集群之间数据的对象传输,最后会判断该对象是否有writeReplace方法,如果有就调用该对象的writeReplace方法

2)累加器中正好实现了这个方法,所以在传输累加器对象时会调用该方法,并在该方法中调用了累加器对象中的copy、reset和isZero方法,就是每个executor都需要复制一份累加器对象,并将对象中的数据进行reset,最后还会判断数据是否为空,如果不为空则报错

至此,本次分享就结束了,我想大家应该大致了解了为什么要用累加器、累加器的作用以及累加器内部方法的作用了。