Spark 共享变量——累加器(accumulator)与广播变量(broadcast variable)

累加器(accumulator)

我们传递给Spark的函数,如map(),或者filter()的判断条件函数,能够利用定义在函数之外的变量,但是集群中的每一个task都会得到变量的一个副本,并且task在对变量进行的更新不会被返回给driver。而Spark的两种共享变量:累加器(accumulator)和广播变量(broadcast variable),在广播和结果聚合这两种常见类型的通信模式上放宽了这种限制。
使用累加器可以很简便地对各个worker返回给driver的值进行聚合。累加器最常见的用途之一就是对一个job执行期间发生的事件进行计数。例如,当我们统计输入文件信息时,有时需要统计空白行的数量。下面的程序描述了这个过程。

import org.apache.spark.{SparkContext, SparkConf}

object AccumulatorTest{
  def main(args: Array[String]) {
    val conf = new SparkConf()
      // .setMaster("spark://node01:7077")
      .setMaster("local")
      .setAppName("accumulatrorTest")
      .setJars(List("E:\\IdeaProjects\\SparkExercise\\out\\artifacts\\SparkExercise_jar\\SparkExercise.jar"))
    val sc = new SparkContext(conf)
    val file = sc.textFile("E:\\file.txt")
    val blankLines = sc.accumulator(0) // Create an Accumulator[Int] initialized to 0,结果返回4
// var blankLines =0 //结果返回0,因为每个task会得到blankLines的一个副本,且每个task对它的更新不会返回给driver
    val callSigns = file.flatMap(line => {
        if (line == "") {
          blankLines += 1 // Add to the accumulator
        }
        line.split(" ")
      })
    callSigns.saveAsTextFile("E:\\output.txt")
    println("Blank lines: " + blankLines)
    sc.stop()
  }
}

在上面的例子中,我们创建了一个名为blankLines的整型累加器(Accumulator[Int]),初始化为0,然后再每次读到一个空白行的时候blankLines加一。因此,累加器使我们可以用一种更简便的方式,在一个RDD的转换过程中对值进行聚合,而不用额外使用一个filter()或reduce()操作。
需要注意的是,由于Spark的lazy机制,只有在saveAsTestFile这个action算子执行后我们才能得到blankLines的正确结果。

由于对于worker节点来说,累加器的值是不可访问的,所有对于worker上的task,累加器是write-only的。这使得累加器可以被更高效的实现,而不需要在每次更新时都进行通信。

当有多个值需要被跟踪记录,或者一个值需要在并行程序的多处进行更新时,使用累加器的计数功能变得尤其方便。例如,原始数据中经常有一部分的无效数据。当无效数据的比例很高时,为了防止产生垃圾输出,我们需要使用累加器对数据中的有效数据和无效数据分别进行计数。接着上面的程序,下面的程序描述了有效数据和无效数据的统计过程,并且为了简便,我们假设以字母 “t”开头的单词是无效数据。

    def validateSign(word:String):Boolean={
      if (word.startsWith("t")){
        invalidLines += 1
        false
      }
      else{
        validLines += 1
        true
      }
    }
    val validSigns = callSigns.filter(validateSign)
    val contactCount = validSigns.map(word => (word,1)).reduceByKey((a,b)=>a+b) contactCount.count() if(invalidLines.value<0.1*validLines.value){ contactCount.saveAsTextFile("E:\\contactCount") }else{ println(f"Too many errors: $invalidLines in $validLines ") } 

累加器与容错

对于失效节点和慢节点,Spark会自动通过重新执行(re-executing)失效任务或慢任务。而有时,即使没有节点失效,Spark可能会需要重新执行一遍tasks来重建一个被移出内存的缓存值,这就导致同一数据上的同一函数可能会因此执行多次。
对于在RDD转换(Transformation)操作中的累加器,一个累加器的更新可能会出现多次。出现这种现象的一种可能情况是,一个被缓存,但是不经常使用的RDD被第一次弹出LRU cache队列,但是之后又需要使用了。这时RDD会根据其血统(lineage)被重新计算,而累加器上的更新也因此多执行了一遍,并返回给driver。因此,建议在转换操作中使用的累加器仅用于调试目的。

自定义累加器

上面的例子中使用的是Spark内建的Integer类型累加器。同时,Spark还支持Double,Long,和Float类型的累加器。除此之外,Spark还提供了自定义累加器类型和聚合操作(如查找最大值等加操作以外的操作)的API,但要保证定义的操作满足交换律和结合律。

广播变量

Spark的另一种共享变量是广播变量。通常情况下,当一个RDD的很多操作都需要使用driver中定义的变量时,每次操作,driver都要把变量发送给worker节点一次,如果这个变量中的数据很大的话,会产生很高的传输负载,导致执行效率降低。使用广播变量可以使程序高效地将一个很大的只读数据发送给多个worker节点,而且对每个worker节点只需要传输一次,每次操作时executor可以直接获取本地保存的数据副本,不需要多次传输。

val signPrefixes = sc.broadcast(loadCallSignTable())
val countryContactCounts = contactCounts.map{case (sign, count) =>
val country = lookupInArray(sign, signPrefixes.value)
(country, count)
}.reduceByKey((x, y) => x + y) countryContactCounts.saveAsTextFile(outputDir + "/countries.txt") 

创建并使用广播变量的过程如下:

  • 在一个类型T的对象obj上使用SparkContext.brodcast(obj)方法,创建一个Broadcast[T]类型的广播变量,obj必须满足Serializable。
  • 通过广播变量的.value()方法访问其值。

另外,广播过程可能由于变量的序列化时间过程或者序列化变量的传输过程过程而成为瓶颈,而Spark Scala中使用的默认的Java序列化方法通常是低效的,因此可以通过spark.serializer属性为不同的数据类型实现特定的序列化方法(如Kryo)来优化这一过程。

参考文献

Learning Spark - O’Reilly Media

你可能感兴趣的:(Spark)