Spark共享变量---累加器(及transformation和action回顾)

一:累加器简介

(一)累加器用途

在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,

如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,

即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,

但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。

累加器是用于跨执行器聚合信息的变量。例如,这些信息可以与数据或API诊断相关,比如有多少记录被破坏了,或者某个特定的库API被调用了多少次。

(二)累加器概念

在Spark中如果想在Task计算的时候统计某些事件的数量,使用filter/reduce也可以,但是使用累加器是一种更方便的方式,累加器一个比较经典的应用场景是用来在Spark Streaming应用中记录某些事件的数量。使用累加器时需要注意只有Driver能够取到累加器的值,Task端进行的是累加操作。(可以认为在task端使用写锁,一次只能一个task写入,不会出现竞争导致数据出错)

Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能,只能累加,不能减少。累加器只能在Driver端构建,并只能从Driver端读取结果,在Task端只能进行累加。

注意:在每个执行器上更新累加器,都会将累加数据转发回Driver驱动程序。(所以为了避免网络传输次数过大,可以将多次更新的值放入本地变量,到达指定数值后,更新给累加器,减少网络传输次数)

(三)累加器概念图

 

二:累加器实现原理

每个Executor各自可以在累加器变量中增加delta值,Executor把delta值发送给Driver,Driver将所有的delta值加在一起。

以下图为例,定义了一个累加器变量VALID,初始值为0。

1.Executor1增加42,Driver接收到之后,VALID的值为42。

2.Executor2增加8,VALID的值为50。Executor3增加10,VALID的值为60。在Driver中读取该变量时,得到的结果为60。

三:累加器案例讲解

(一)前提

处理目录下的日志文件,某些日志被破坏,导致部分行数据丢失,为空白。我们要统计出来一共有多少行数据被破坏。

(二)不使用累加器的代码(分析错误)---重点

读取some log file目录下的所有日志,甚至分区为4.

上述代码的问题是,当Driver驱动程序打印变量blankLines时,它的值将是零。

这是因为,当Spark将这段代码发送给每个Task执行程序时,这些变量将成为该执行程序的局部变量,并且其更新后的值不会被转发回驱动程序。

为了避免这个问题,我们需要在累加器中设置变量:空白行,这样每个执行器中对这个变量的所有更新都将被转发回驱动程序。

(三)改进用累加器

这保证了在每个Task执行器上更新累加器空白行,并将更新转发回驱动程序。

熟悉Hadoop Map-Reduce的人会注意到Spark的累加器类似于Hadoop的Map-Reduce计数器。

三:累加器注意事项 

(一) 累加器在Driver端定义赋初始值,累加器只能在Driver端读取最后的值,在Excutor端更新。

(二)累加器不是一个调优的操作因为如果不这样做,结果是错的(必须这样做)

(三)累加器没有改变Spark的lazy计算模型,如果累计器在RDD的操作中更新了,累计器的值只会在RDD作为action的一部分被计算时更新。所以,在lazy的transformation中(如map()),累加器的更新不能保证被执行。

为了安全起见,始终只在action中使用累加器。

(四)更多关于transformation和action可以见四中

四:累加器的使用

(一)累加器累加一个数组

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

(二)累加器错误用法---少加(transformation)

    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()),累加器的更新不能保证被执行

(三)累加器错误用法---多加(transformation和action)

    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操作就会读取缓存的数据集,而无需从头开始计算。

(四)使用cache保持数据一致

    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)

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

(五)如果累计器在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

(六)对比transformation和action

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

(七)transformation和action补充:聚合算子

def method2(lines: RDD[String]): Unit = {
 
    val totalAcc: LongAccumulator = lines.sparkContext.longAccumulator("total")  //定义累加器
 
    val dbRDD: RDD[String] = lines.filter(line => {  //filter transformation延时计算
      val fields = line.split("\\^")
      val jobs = fields(1)
      jobs.contains("大数据") || jobs.contains("hadoop") || jobs.contains("spark")
    })
 
 
    val eduRDD = dbRDD.map( line => {    //map transformation延时计算
      totalAcc.add(1)
      val fields = line.split("\\^")
      val edu = fields(6)
      (edu, 1)
    }).reduceByKey(_ + _)  //reduce transformation延时计算
    //确保累加器执行
    eduRDD.count()  //count action直接触发计算
    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,累加类不再进行累加。

五:内置累加器

上面的代码使用了内置支持的Long类型累加器,当然还存在其他内置支持的累加器:double类型、集合Collection类型

(一)集合累加器---官方提供的一个自定义累加器案例

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("wordcount").setMaster("local[2]")
    val sc = new SparkContext(conf)
    case class User(name:String,telephone:String)
    val collect_plus=Array(User("Alice","15837312345")  //这是我们将要进行累加的值
      ,User("Bob","13937312666")
      ,User("Thomas","13637312345")
      ,User("Tom","18537312777")
      ,User("Boris","13837312998")
    )
    val rdd05=sc.parallelize(collect_plus,2)
    val user_plustool=sc.collectionAccumulator[User]("用户累加器")  //定义累加器

    rdd05.foreach(user => {
      val teletephone=user.telephone.reverse
      if (teletephone(0)==teletephone(1) && teletephone(0)==teletephone(2)){
        user_plustool.add(user)  //进行累加
      }
    }
    )
    println(user_plustool)
  }
}

六:自定义累加器

(一)自定义累加器讲解

可以通过AccumulatorV2创建自己的类型。AccumulatorV2抽象类由多个方法,其中必须重写的是:

reset:用于重置累加器为0

add:用于向累加器加一个值

merge:用于合并另一个同类型的累加器到当前累加器

其它必须重写的方法有:

copy():创建此累加器的新副本

isZero():返回该累加器是否为零值

value():获取此累加器的当前值

(二)自定义累加器实现

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

package com.rk.spark
 
import java.util
 
import org.apache.spark.util.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}
 
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)  //返回为0,没有匹配成功,过滤掉。返回1,匹配成功,保留
        if (!flag) {
          myAccum.add(line)  //将我们filter要过滤掉的数据保存到累加器中
        }
        flag
      }).map(_.toInt).reduce(_ + _)
    println("sum: " + sum)
    for (v <- myAccum.value.toArray) {
      print(v + " ")
    }
    println()
    sc.stop()
  }
}

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

sum: 10
4f a5 2a 

 

posted @ 2020-03-30 17:39  山上有风景  阅读(1323)  评论(0编辑  收藏  举报