spark中使用Accumulator累加器使用和注意事项

Accumulator简介

Accumulator是spark提供的累加器,累加器的一个常用用途是在调试时对作业执行过程中的事件进行计数,但是只要driver能获取Accumulator的值(使用value方法), Task只能对其做增加操作(使用+=),也可以在为Accumulator命名(不支持Python),这样就会在spark web ui中显示, 可以帮助了解程序运行的情况。

数值累加器可通过调用SparkContext.longAccumulator()或SparkContext,doubleAccumulator()创建,分别用于累加Long或Double类型的值。运行在集群上的任务之后可使用add方法进行累加操作。但是,这些任务并不能读取累加器的值,只有驱动程序使用value方法能读取累加器的值。

Accumulator使用

使用示例:

累加器累加一个数组:

    //在driver中定义
    val acc = sc.longAccumulator("Test Accumulator")
    
    //在task中进行累加
    sc.parallelize(1 to 10).foreach(x => acc.add(x))
    
    //在driver中输出
    println(acc.value)

累加器的错误用法:

少加的情况:

    val accum = sc.longAccumulator("Error Accumulator")
    val numberRDD = sc.parallelize(1 to 10).map(n => {
      accum.add(1)
      n + 1
    })
    println("accumulator: " + accum.value)

执行完毕,打印的值时多少呢?答案是0,累加器没有改变Spark的lazy计算模型,如果累计器在RDD的操作中更新了,累计器的值只会在RDD作为action的一部分被计算时更新。所以,在lazy的transformation中(如map()),累加器的更新不能保证被执行。

多加的情况:

    val accum = sc.longAccumulator("Error2 Accumulator")
    val numberRDD = sc.parallelize(1 to 10).map(n => {
      accum.add(1)
      n + 1
    })
    numberRDD.count()
    println("accum1: " + accum.value)
    numberRDD.reduce(_+_)
    println("accum2: " + accum.value)

结果得到:

accum1:10

accum2: 20

虽然只在map里进行了累加器加1的操作,但是两次得到的累加器的值却不一样,这是由于count和reduce都是action类型的操作,触发了两次作业的提交,所以map算子实际上被执行了两次,在reduce操作提交作业后累加器又完成了一轮技数,所以最终的累加器的值为20。究其原因是因为count虽然促使numberRDD累计出来,但是由于没有对其进行缓存,所以下次再次需要使用numberRDD这个数据集时,还需要从并行化数据集的部分开始执行计算。解释道这里,这个问题的解决方法也就很清楚了,就是在count之前调用numberRDD的cache方法(或persist),这样在count后数据集就会被缓存下来,reduce操作就会读取缓存的数据集,而无需从头开始计算。

代码实现如下:

    val accum = sc.longAccumulator("Error2 Accumulator")
    val numberRDD = sc.parallelize(1 to 10).map(n => {
      accum.add(1)
      n + 1
    })
    numberRDD.cache().count()
    println("accum1: " + accum.value)
    numberRDD.reduce(_+_)
    println("accum2: " + accum.value)

这次两次打印的值就会保持一致了。

如果累计器在actions操作算子里面执行时,只会累加一次:

    val accum = sc.longAccumulator("Error2 Accumulator")
    val numberRDD = sc.parallelize(1 to 10).map(n => {
      n + 1
    })
    numberRDD.foreach(t =>{
      accum.add(1)
    })
    println("accum1: " + accum.value)
    numberRDD.reduce(_+_)
    println("accum2: " + accum.value)

结果得到:

accum1: 10

accum2: 10

注意:

对于只在actions执行更新操作的累加器,Spark会保证任务对累加器的更新操作只会应用一次,例如,重启任务不会更新累加器的值。在transformations中用户应该意识到,如果任务或作业阶段重新执行,每个任务的更新操作会应用多次。

自定义累加器:

上面的代码使用了内置支持的Long类型累加器,我们也可以通过AccumulatorV2创建自己的类型。AccumulatorV2抽象类由多个方法,其中必须重写的是:reset,用于充值累加器为0.add用于向累加器加一个值,merge用于合并另一个同类型的累加器到当前累加器,其它必须重写的方法有copy()(Create a new copy of this accumulator),isZero()(Returns if this accumulator is zero value or not),value()(Defines the current value of this accumulator)

在spark2.0版本后,累加器的易用性有较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。官方同时给出了一个实现的示例:CollectionAccumulator类,这个类允许以集合的形式收集spark应用执行过程中的一些信息。例如,我们可以用这个类收集Spark处理数据时的一些细节,当然,由于累加器的值最终要汇聚到driver端,为了避免driver端的outOfMemory问题,需要对收集的信息的规模要加以控制,不宜过大。

实现自定义类型累加器需要继承AccumulatorV2并至少重写下面的方法

/**
  * @program: bd1809
  * @class_name LogAccumulator
  * @author: rk
  * @create: 2019-03-05 16:25
  * @Description:
  *
  *   isZero: 当AccumulatorV2中存在类似数据不存在这种问题时,是否结束程序。
  *   reset: 重置AccumulatorV2中的数据
  *   add: 操作数据累加方法实现
  *   merge: 合并数据
  *   value: AccumulatorV2对外访问的数据结果
  *   copy: 拷贝一个新的AccumulatorV2
  *
  **/
class MyAccumulatorV2 extends AccumulatorV2[String, String] {

  override def isZero: Boolean = ???

  override def copy(): AccumulatorV2[String, String] = ???

  override def reset(): Unit = ???

  override def add(v: String): Unit = ???

  override def merge(other: AccumulatorV2[String, String]): Unit = ???

  override def value: String = ???

}

下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。

package com.rk.spark

import java.util

import org.apache.spark.util.AccumulatorV2

/**
  * @program: bd1809
  * @class_name LogAccumulator
  * @author: rk
  * @create: 2019-03-05 16:25
  * @Description:
  *
  *   isZero: 当AccumulatorV2中存在类似数据不存在这种问题时,是否结束程序。
  *   reset: 重置AccumulatorV2中的数据
  *   add: 操作数据累加方法实现
  *   merge: 合并数据
  *   value: AccumulatorV2对外访问的数据结果
  *   copy: 拷贝一个新的AccumulatorV2
  *
  **/
class MyAccumulatorV2 extends AccumulatorV2[String, java.util.Set[String]]{

  private val set:java.util.Set[String] = new util.HashSet[String]()

  override def isZero: Boolean = {
    set.isEmpty
  }

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

  override def add(v: String): Unit = {
    set.add(v)
  }

  override def merge(other: AccumulatorV2[String, util.Set[String]]): Unit = {
    other match {
      case o:MyAccumulatorV2 => set.addAll(o.value)
    }
  }

  override def value: java.util.Set[String] = {
    java.util.Collections.unmodifiableSet(set)
  }

  override def copy(): AccumulatorV2[String, util.Set[String]] = {
    val newAcc = new MyAccumulatorV2()
    set.synchronized{
      newAcc.set.addAll(set)
    }
    newAcc
  }

}

测试类:

package com.rk.spark

import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @program: bd1809
  * @class_name AccumulatorTest
  * @author: rk
  * @create: 2019-03-05 15:37
  * @Description:
  **/
object AccumulatorTest {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)
    Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
    Logger.getLogger("org.spark-project").setLevel(Level.OFF)
    val conf = new SparkConf()
      .setMaster("local[*]")
      .setAppName(this.getClass.getSimpleName)
    val sc = new SparkContext(conf)

    //自定义
    val myAccum = new MyAccumulatorV2
    sc.register(myAccum, "MyAccumulator")
    val sum: Int = sc.parallelize(
      Array("1", "2a", "3", "4f", "a5", "6", "2a"), 2)
      .filter(line => {
        val pattern = """^-?(\d+)"""
        val flag = line.matches(pattern)
        if (!flag) {
          myAccum.add(line)
        }
        flag
      }).map(_.toInt).reduce(_ + _)
    println("sum: " + sum)
    for (v <- myAccum.value.toArray) {
      print(v + " ")
    }
    println()
    sc.stop()
  }
}

本例中利用自定义的收集器收集过滤操作中被过滤掉的元素,当然这部分的元素的数据量不能太大。

结果如下:

sum: 10
4f a5 2a 

 

注意:

在测试多加情况时,发现并不是触发多个action操作,就会一直累加?

def method2(lines: RDD[String]): Unit = {

    val totalAcc: LongAccumulator = lines.sparkContext.longAccumulator("total")

    val dbRDD: RDD[String] = lines.filter(line => {
      val fields = line.split("\\^")
      val jobs = fields(1)
      jobs.contains("大数据") || jobs.contains("hadoop") || jobs.contains("spark")
    })


    val eduRDD = dbRDD.map( line => {
      totalAcc.add(1)
      val fields = line.split("\\^")
      val edu = fields(6)
      (edu, 1)
    }).reduceByKey(_ + _)
    //确保累加器执行
    eduRDD.count()
    val sum = totalAcc.value
    println(sum)
    eduRDD.foreach{ case (edu, count) =>{
      println(s"学历是${edu}的人数为${count},占得比例为: "+count/(sum+0.0))
    }}

    println(totalAcc.value)


  }

上面代码结果为sum=448,但是经过foreach()后,得到的totalAcc.value的结果仍然是448?

分析:测试发现eduRDD是dbRDD经过map和reduceByKey两个算子得到的结果,所以eduRDD经过多次action算子也不会重复累加了,但是如果将map和reduceByKey算子分开,并且是在map中添加的累加器,那重复调用reduceByKey后生成的RDD的action算子,将会翻倍累加。(eduRDD1.reduceByKey,其中eduRDD1是通过dbRDD.map添加累加器后生成的),此时为重复调用eduRDD1,即重复进行累加,但是连着使用两个算子后,调用将不会重复累加。通过测试发现当经过聚合算子的操作后,得到的RDD重复调用action,累加类不再进行累加,具体原理以后还待考核。。。

你可能感兴趣的:(大数据)