Spark:2.10中使用累加器、注意点以及实现自定义累加器Accumulator

Spark累加器剖析(1) —— 介绍 & 重点类 & 源码解析:https://blog.csdn.net/lemonZhaoTao/article/details/81256413

Spark累加器剖析(2) —— 累加器执行的过程:https://blog.csdn.net/lemonZhaoTao/article/details/81273243

Spark累加器剖析(3) —— 自定义累加器:https://blog.csdn.net/lemonZhaoTao/article/details/81407782

Spark累加器剖析(4) —— 累加器使用过程中的坑:https://blog.csdn.net/lemonZhaoTao/article/details/81677153

 

累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。

累加器是仅仅被相关操作累加的变量,因此可以在并行中被有效地支持。它可以被用来实现计数器和总和。

Spark原生地只支持数字类型的累加器,编程者可以添加新类型的支持。

如果创建累加器时指定了名字,可以在Spark的UI界面看到。

这有利于理解每个执行阶段的进程。(对于python还不支持)
累加器通过对一个初始化了的变量v调用SparkContext.accumulator(v)来创建。

在集群上运行的任务可以通过add或者”+=”方法在累加器上进行累加操作。

但是,它们不能读取它的值。

只有驱动程序能够读取它的值,通过累加器的value方法。

Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。

Accumulator只提供了累加的功能,只能累加,不能减少。

累加器只能在Driver端构建,并只能从Driver端读取结果,在Task端只能进行累加。

 

至于这里为什么只能在Task累加呢?下面的内容将会进行详细的介绍,先简单介绍下:
在Task节点,准确的就是说在executor上;

每个Task都会有一个累加器的变量,被序列化传输到executor端运行之后再返回过来都是独立运行的;

如果在Task端去获取值的话,只能获取到当前Task的,Task与Task之间不会有影响。

累加器不会改变Spark lazy计算的特点,只会在Job触发的时候进行相关的累加操作

注意:

  1. 首先要创建累加器的对象初始值是0
  2. 驱动器(driver program)程序可以调用累加器的value属性
  3. 累加器是一个只写变量
  4. 累加器最好还是不要在transform操作中使用:原因

 (在transform操作中spark有可能会重复进行计算,例如如果有一个节点崩溃了,spark会自动将任务运行在另外一个节点上,这就造成了累加器有可能会被重复用到,因此好的办法是将累加器放在Action操作中的foreach里面

==========================================================================================

共享变量

通常情况下,当向Spark操作(如map,reduce)传递一个函数时,它会在一个远程集群节点上执行,它会使用函数中所有变量的副本。这些变量被复制到所有的机器上,远程机器上并没有被更新的变量会向驱动程序回传。在任务之间使用通用的,支持读写的共享变量是低效的。尽管如此,Spark提供了两种有限类型的共享变量,广播变量和累加器。

 

广播变量

广播变量允许程序员将一个只读的变量缓存在每台机器上,而不用在任务之间传递变量。广播变量可被用于有效地给每个节点一个大输入数据集的副本。Spark还尝试使用高效地广播算法来分发变量,进而减少通信的开销。

Spark的动作通过一系列的步骤执行,这些步骤由分布式的洗牌操作分开。Spark自动地广播每个步骤每个任务需要的通用数据。这些广播数据被序列化地缓存,在运行任务之前被反序列化出来。这意味着当我们需要在多个阶段的任务之间使用相同的数据,或者以反序列化形式缓存数据是十分重要的时候,显式地创建广播变量才有用。


通过在一个变量v上调用SparkContext.broadcast(v)可以创建广播变量。广播变量是围绕着v的封装,可以通过value方法访问这个变量。举例如下:

 

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

 

在创建了广播变量之后,在集群上的所有函数中应该使用它来替代使用v.这样v就不会不止一次地在节点之间传输了。另外,为了确保所有的节点获得相同的变量,对象v在被广播之后就不应该再修改。

 

累加器

累加器是仅仅被相关操作累加的变量,因此可以在并行中被有效地支持。它可以被用来实现计数器和总和。Spark原生地只支持数字类型的累加器,编程者可以添加新类型的支持。如果创建累加器时指定了名字,可以在Spark的UI界面看到。这有利于理解每个执行阶段的进程。(对于python还不支持)

累加器通过对一个初始化了的变量v调用SparkContext.accumulator(v)来创建。在集群上运行的任务可以通过add或者"+="方法在累加器上进行累加操作。但是,它们不能读取它的值。只有驱动程序能够读取它的值,通过累加器的value方法。

下面的代码展示了如何把一个数组中的所有元素累加到累加器上:

 

scala> val accum = sc.accumulator(0, "My Accumulator")
accum: spark.Accumulator[Int] = 0

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x)
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Int = 10

 

尽管上面的例子使用了内置支持的累加器类型Int,但是开发人员也可以通过继承AccumulatorParam类来创建它们自己的累加器类型。AccumulatorParam接口有两个方法:

zero方法为你的类型提供一个0值。

addInPlace方法将两个值相加。

假设我们有一个代表数学vector的Vector类。我们可以向下面这样实现:

 

object VectorAccumulatorParam extends AccumulatorParam[Vector] {
  def zero(initialValue: Vector): Vector = {
    Vector.zeros(initialValue.size)
  }
  def addInPlace(v1: Vector, v2: Vector): Vector = {
    v1 += v2
  }
}

// Then, create an Accumulator of this type:
val vecAccum = sc.accumulator(new Vector(...))(VectorAccumulatorParam)

在Scala里,Spark提供更通用的累加接口来累加数据,尽管结果的类型和累加的数据类型可能不一致(例如,通过收集在一起的元素来创建一个列表)。同时,SparkContext..accumulableCollection方法来累加通用的Scala的集合类型。

 

累加器仅仅在动作操作内部被更新,Spark保证每个任务在累加器上的更新操作只被执行一次,也就是说,重启任务也不会更新。在转换操作中,用户必须意识到每个任务对累加器的更新操作可能被不只一次执行,如果重新执行了任务和作业的阶段。

累加器并没有改变Spark的惰性求值模型。如果它们被RDD上的操作更新,它们的值只有当RDD因为动作操作被计算时才被更新。因此,当执行一个惰性的转换操作,比如map时,不能保证对累加器值的更新被实际执行了。下面的代码片段演示了此特性:

 

val accum = sc.accumulator(0)
data.map { x => accum += x; f(x) }
//在这里,accum的值仍然是0,因为没有动作操作引起map被实际的计算.

原文参考:https://blog.csdn.net/sdgihshdv/article/details/78312598

 

===========================================================================

 

累加器简单使用

Spark内置的提供了Long和Double类型的累加器(没有int?scala有int)。下面是一个简单的使用示例,在这个例子中我们在过滤掉RDD中奇数的同时进行计数,最后计算剩下整数的和。

 

    val sparkConf = new SparkConf().setAppName("Test").setMaster("local[2]")
    val sc = new SparkContext(sparkConf)
    val accum = sc.longAccumulator("longAccum") //统计奇数的个数
    val sum = sc.parallelize(Array(1,2,3,4,5,6,7,8,9),2).filter(n=>{
      if(n%2!=0) accum.add(1L) 
      n%2==0
    }).reduce(_+_)
 
    println("sum: "+sum)
    println("accum: "+accum.value)
 
    sc.stop()

结果为:

sum: 20
accum: 5

这是结果正常的情况,但是在使用累加器的过程中如果对于spark的执行过程理解的不够深入就会遇到两类典型的错误:少加(或者没加)、多加。

少加的情况:

对于如下代码:

    val accum = sc.longAccumulator("longAccum")
    val numberRDD = sc.parallelize(Array(1,2,3,4,5,6,7,8,9),2).map(n=>{
      accum.add(1L)
      n+1
    })
    println("accum: "+accum.value)



执行完毕,打印的值是多少呢?答案是0,因为累加器不会改变spark的lazy的计算模型,即在打印的时候像map这样的transformation还没有真正的执行,从而累加器的值也就不会更新。

 

多加的情况:

对于如下代码:

    val accum = sc.longAccumulator("longAccum")
    val numberRDD = sc.parallelize(Array(1,2,3,4,5,6,7,8,9),2).map(n=>{
      accum.add(1L)
      n+1
    })
    numberRDD.count
    println("accum1:"+accum.value)
    numberRDD.reduce(_+_)
    println("accum2: "+accum.value)

结果我们得到了:

 

accum1:9

accum2: 18

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

 

    val accum = sc.longAccumulator("longAccum")
    val numberRDD = sc.parallelize(Array(1,2,3,4,5,6,7,8,9),2).map(n=>{
      accum.add(1L)
      n+1
    })
    numberRDD.cache().count
    println("accum1:"+accum.value)
    numberRDD.reduce(_+_)
    println("accum2: "+accum.value)


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

 

自定义累加器

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

实现自定义类型累加器需要继承AccumulatorV2并至少覆写下例中出现的方法,下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。

import java.util
 
import org.apache.spark.util.AccumulatorV2
 
class LogAccumulator extends AccumulatorV2[String, java.util.Set[String]] {
  private val _logArray: java.util.Set[String] = new java.util.HashSet[String]()
 
  override def isZero: Boolean = {
    _logArray.isEmpty
  }
 
  override def reset(): Unit = {
    _logArray.clear()
  }
 
  override def add(v: String): Unit = {
    _logArray.add(v)
  }
 
  override def merge(other: AccumulatorV2[String, java.util.Set[String]]): Unit = {
    other match {
      case o: LogAccumulator => _logArray.addAll(o.value)
    }
 
  }
 
  override def value: java.util.Set[String] = {
    java.util.Collections.unmodifiableSet(_logArray)
  }
 
  override def copy(): AccumulatorV2[String, util.Set[String]] = {
    val newAcc = new LogAccumulator()
    _logArray.synchronized{
      newAcc._logArray.addAll(_logArray)
    }
    newAcc
  }
}


测试类:

import scala.collection.JavaConversions._
 
import org.apache.spark.{SparkConf, SparkContext}
 
object Main {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("Test").setMaster("local[2]")
    val sc = new SparkContext(sparkConf)
    val accum = new LogAccumulator
    sc.register(accum, "logAccum")
    val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line => {
      val pattern = """^-?(\d+)"""
      val flag = line.matches(pattern)
      if (!flag) {
        accum.add(line)
      }
      flag
    }).map(_.toInt).reduce(_ + _)
 
    println("sum: " + sum)
    for (v <- accum.value) print(v + " ")
    println()
    sc.stop()
  }
}


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

sum; 32
7cd 4b 2a 

原文参考:https://blog.csdn.net/u013468917/article/details/70617085

 

=====================================================================================

 

Accumulator(累加器, 计数器)
类似于MapReduce中的counter, 将数据从一个节点发送到其他各个节
点上去;

通常用于监控, 调试, 记录符合某类特征的数据数目等

累加器在Driver端被读取,使用的是 Accumulator.value

累加器在Executor端被读取,使用的是 Accumulator.localValue,获取的是Executor本地的值。Executor端可以通过 Accumulator+=1的方式进行累加(Spark1.6方式)

 

  创建累加器 对累加器进行累加 Driver端获取累加器 Executor端获取累加器
Spark1.6 sc.accumulator(0L,"counter0") counter1 += 1 println("counter1="+counter1)  println("counter1="+counter1.localValue)
Spark2.1 sc.longAccumulator("total_counter") total_counter.add(1) println("total_counter="+total_counter.value)  println("total_counter="+total_counter.value)

 

累加器的使用陷阱:

我们都知道,spark中的一系列transform操作会构成一串长的任务链,此时需要通过一个action操作来触发,accumulator也是一样。因此在一个action操作之前,你调用value方法查看其数值,肯定是没有任何变化的。

如果程序中有两次 action操作,就会触发两次transform操作,相应地,累加器就会加两次。问题代码如下:

 

val accum= sc.accumulator(0, "Error Accumulator")
val data = sc.parallelize(1 to 10)
//用accumulator统计偶数出现的次数,同时偶数返回0,奇数返回1
val newData = data.map{x => {
  if(x%2 == 0){
    accum += 1
      0
    }else 1
}}
//使用action操作触发执行
newData.count
//此时accum的值为5,是我们要的结果
accum.value
 
//继续操作,查看刚才变动的数据,foreach也是action操作
newData.foreach(println)
//上个步骤没有进行累计器操作,可是累加器此时的结果已经是10了
//这并不是我们想要的结果
accum.value

 

累加器陷阱解决办法:

将任务之间的依赖关系切断,再次执行action操作就可以了

 

val accum= sc.accumulator(0, "Error Accumulator")
val data = sc.parallelize(1 to 10)
 
//代码和上方相同
val newData = data.map{x => {...}}
//使用cache缓存数据,切断依赖。
newData.cache.count
//此时accum的值为5
accum.value
 
newData.foreach(println)
//此时的accum依旧是5
accum.value

 

调用cache,persist方法的时候会将之前的依赖切除,后续的累加器就不会再被之前的transfrom操作影响到了。

原文参考:https://blog.csdn.net/sunspeedzy/article/details/69943467

其他:https://blog.csdn.net/csdnmrliu/article/details/82837622

            https://www.csdn.net/gather_25/MtTaYg3sMDE3Ni1ibG9n.html

 

你可能感兴趣的:(spark)