Spark三大核心数据结构(二)——累加器 & 自定义累加器的使用原理

Spark的三大核心数据结构:RDD、累加器(只写不读)、广播变量(只读不写)

在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,即在分布式运行时每个task运行的知识原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。

摘要:

(1)自定义变量在Spark中运算时,会从Driver中复制一份副本到Executor中运算,但变量的运算结果并不会返回给Driver,所以无法实现自定义变量的值改变,一直都是初始值,所以针对这个问题,引入了累加器的概念;

(2)系统累加器longAccumulator和自定义累加器(extends AccumulatorV2[类型,类型])实际都是两步,new累加器,然后sc.register注册累加器;

(3)先在Driver程序中创建一个值为0或者空的累加器对象,Task运算时,Executor中会copy一份累加器对象,在Executor中进行运算,累加器的运算结果返回给Driver程序并合并Merge,得出累加器最终结果;

(4)累加器.add(元素);具体对元素的操作包括数据sum、增加、删减、筛选等要求,都可以写在自定义累加器的.add()方法中。

目录

1. 累加器原理

2. 累加器原理图:

3. 自定义累加器

4. 自定义累加器——用法2:筛选数据

5. 累加器的Shell实现


1. 累加器原理

贴一段代码如下,来说明累加器的原理

object Spark_Add {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[*]").setAppName("Application")
        //构建Spark上下文对象
        val sc = new SparkContext(conf)

        var sum = 0
        val rdd = sc.makeRDD(Array(1,2,3,4,5))

        rdd.map(item=>{
            sum = sum + item
        }).collect()
        println("sum = "+sum)

        //释放资源
        sc.stop()
    }
}

-----------------------------------
sum = 0

在Spark中声明SparkContext的类称为Driver,所以变量sum在Driver中;

而任务Task(即分区数据的运算)的执行是在Executor中进行,即sum = sum + item在Executor节点执行;

为什么Task运算完后,在Driver的IDEA客户端打印结果sum还是0呢?

问题的关键点在于:Executor只是做了运算,但并没有将sum运算后的值返回Driver中,也就是说Driver中的sum变量至始至终都保持初始值为0;

那么Spark中怎么解决将Executor中运算完毕的数据传回Driver中修改原始数据呢?这里就引入了“累加器”的概念

object Spark_Add {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[*]").setAppName("Application")
        //构建Spark上下文对象
        val sc = new SparkContext(conf)

        //使用系统默认累加器,默认初始值为0
        val sum = sc.longAccumulator("sum")

        val rdd = sc.makeRDD(Array(1,2,3,4,5))

        rdd.map(item=>{
            sum.add(item)
        }).collect()
        println("sum = "+sum.value)

        //释放资源
        sc.stop()
    }
}
-------------------------------------
sum = 15

因为累加器sum在Driver中,Executor中运算Task时,会把sum作为副本从Driver传递到Executor中,通过sum.add方法累加rdd元素;

并且会将各个Executor运算结果返回Driver,并作Merge合并操作,所以Driver中最终sum数据就是各个Executor运算合并后的结果。

2. 累加器原理图:

直接在Driver中声明共享变量,运算时会将其copy一份副本到各个Executor中,但是运算后不会将其返回;

如果共享数据以累加器的方式存在,那么copy运算后,还会从Executor中返回给Driver,实现Merge操作。

累加器的传递途径:序列化

 

3. 自定义累加器

上面的代码使用了系统默认累加器val sum = sc.longAccumulator("sum")

查看其源码:

  def longAccumulator(name: String): LongAccumulator = {
    val acc = new LongAccumulator
    register(acc, name)
    acc
  }

longAccumulator方法中,其实封装了两步,第一步new累加器对象,第二步完成累加器在当前SparkContext中注册


下面自定义累加器,也是用这两步

//累加器
object Spark_Add {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[*]").setAppName("Application")
        //构建Spark上下文对象
        val sc = new SparkContext(conf)

        //创建累加器
        val sum = new MyAccumulator()

        //注册累加器
        sc.register(sum,"accumulator")

        val rdd = sc.makeRDD(Array(1,2,3,4,5))

        rdd.map(item=>{
            sum.add(item)
        }).collect()
        println("sum = "+sum.value)

        //释放资源
        sc.stop()
    }
}
-----------------------------------------------
sum = 15

//自定义累加器
class MyAccumulator extends AccumulatorV2[Int,Int]{
    var sum = 0

    //1. 是否初始状态(sum为0表示累加器为初始状态)
    override def isZero: Boolean = sum == 0

    //2. 执行器执行时需要拷贝累加器对象(把累加器对象序列化后,从Driver传到Executor)
    override def copy(): AccumulatorV2[Int,Int] = {
        val mine = new MyAccumulator
        mine
    }

    //3. 重置数据(重置后看当前累加器是否为初始状态)
    override def reset(): Unit = sum = 0

    //累加数据
    override def add(v: Int): Unit = {
        sum = sum + v
    }

    //合并计算结果数据(把所有Executor中累加器value合并)
    override def merge(other: AccumulatorV2[Int, Int]): Unit = {
        sum = sum + other.value
    }

    //累加器的结果
    override def value: Int = sum
}

自定义累加器需要继承AccumulatorV2[类型,类型],重写所有方法即可;

可以看出创建自定义累加器,后需要sc注册该累加器,与系统默认累加器longAccumulator源码中封装的两步一致。

4. 自定义累加器——用法2:筛选数据

原理:累加器变量初始值为0或者空,通过add方法增加元素;

数据增加、删减、筛选的逻辑都可以写在自定义累加器的add方法中。

eg:写一个小demo,给定一个场景,从字符串中筛选出含b的元素

object Spark_Add {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[*]").setAppName("Application")
        //构建Spark上下文对象
        val sc = new SparkContext(conf)

        //创建累加器
        val acc2 = new MyBlackAccumulator()

        //注册累加器
        sc.register(acc2, "accumulator")

        val rdd = sc.makeRDD(Array("abc", "bcd", "efg"))

        rdd.map(s => {
            acc2.add(s)
        }).collect()
        println("sum = " + acc2.value)

        //释放资源
        sc.stop()
    }
}
---------------------------------------------------
sum = [bcd, abc]


//自定义累加器
//传入元素String,返回的是String集合,要求无序不可重复,用java中hashSet
class MyBlackAccumulator extends AccumulatorV2[String, java.util.HashSet[String]] {
    var blackList = new util.HashSet[String]()

    override def isZero: Boolean = {
        blackList.isEmpty
    }

    override def copy(): AccumulatorV2[String, util.HashSet[String]] = {
        val acc = new MyBlackAccumulator
        acc
    }

    override def reset(): Unit = {
        blackList.clear()
    }

    //包含b的加入黑名单,筛选逻辑写在add中
    override def add(v: String): Unit = {
        if (v.contains("b")) {
            blackList.add(v)
        }
    }

    override def merge(other: AccumulatorV2[String, util.HashSet[String]]): Unit = {
        //把另外集合中数据合并,addAll方法
        blackList.addAll(other.value)
    }

    override def value: util.HashSet[String] = blackList
}

 

5. 累加器的Shell实现

val a = sc.accumulator(0)

创建一个初始值为0的累加器

猜你喜欢

转载自blog.csdn.net/wx1528159409/article/details/87817785