在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变(broadcast variable)和累加器(accumulator)

广播变量用于高效分发较大的对象。对于executor来说广播变量是只读变量。写了无效。

累加器用来对信息进行聚合操作。对于executor来说累加器是只写变量,读了不可能。使用累机器需要注意容错性。

一、广播变量broadcast variable

如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么知识每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

这基本上是一个广播变量的经典用法,将敏感词典下发,用于过滤主文本中的敏感词。

import java.io.{File, FileReader}
import java.util

import org.apache.spark.SparkConf
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.sql.SparkSession

import scala.collection.mutable.ArrayBuffer


object SparkTest{

  // 使用广播变量过滤 敏感数据
  def main(args: Array[String]): Unit = {

    val conf = new SparkConf().setMaster("local[2]").setAppName("test")
    val spark = SparkSession.builder().config(conf).getOrCreate()
    spark.sparkContext.setLogLevel("WARN")

    //计算逻辑
    compute(spark)

    spark.stop()
  }

  def compute(spark:SparkSession):Unit ={
    import spark.implicits._

    //加载过敏词汇并存储到 ArrayList 中
    val filterDataPath = "G:\\tmp\\b.txt"
    val al = new util.ArrayList[String]()
    val reader = new java.io.BufferedReader(new FileReader(new File(filterDataPath)))
    while (reader.ready()){
      val str = reader.readLine()
      str.split(" ").foreach(al.add(_))
    }
    val broadcast: Broadcast[util.ArrayList[String]] = spark.sparkContext.broadcast(al)


    spark.sparkContext.textFile("G:\\tmp\\a.txt")
      .mapPartitions(ite =>{
        // 对源数据进行切割分词,并对每个词进行校验,符合要求的词汇添加到ArrayBuffer中
        val arr = ArrayBuffer[String]()
        val filterWord:util.ArrayList[String] = broadcast.value

        ite.foreach(line => {
          line.split(" ").foreach(word => {
            if( !filterWord.contains(word) ) arr.+=(word)
          })
        })

        arr.toIterator
    })
      .collect
      .foreach(println)
  }

}

注意事项

1、变量一旦被定义为一个广播变量,那么这个变量只能读,不能修改

2、能不能将一个RDD使用广播变量广播出去?

不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。

3、 广播变量只能在Driver端定义,不能在Executor端定义。

4、 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。

5、如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。

6、如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。

二、累加器

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

//代码就不贴了,很多示例代码表达的都有问题。
比如:一个读文件rdd,做map操作,然后一行累加器就加1,再执行action操作,这样有必要吗?
更经典的作用应用是统计下空行的个数。

//map start...

if(line.trim().lenth != 0){
	accum += 1
}

//map end...

xxxrdd.count
println(accum.value)

//这样既聚合了整个rdd的信息,同时聚合了整个文件中所有非空行的信息。

还有一个累计器的实际项目操作,参考整个大神的总结https://www.iteye.com/blog/bit1129-2198682

注意事项

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

2、累加器不是一个调优的操作,因为如果不这样做,结果是错的

3、使用Accumulator时,为了保证准确性,只使用一次action操作。如果需要使用多次则使用cache或persist操作切断依赖。