7.1. 累加器
一个小问题
var count = 0
val config = new SparkConf().setAppName("ip_ana").setMaster("local[6]")
val sc = new SparkContext(config)
sc.parallelize(Seq(1, 2, 3, 4, 5))
.foreach(count += _)
println(count)
(笔记:count += _ 会被分发到多个task,每个task的初始值count=0,这样的计算是不正确的)
上面这段代码是一个非常错误的使用, 请不要仿照, 这段代码只是为了证明一些事情
先明确两件事, var count = 0
是在 Driver 中定义的, foreach(count += _)
这个算子以及传递进去的闭包运行在 Executor 中
这段代码整体想做的事情是累加一个变量, 但是这段代码的写法却做不到这件事, 原因也很简单, 因为具体的算子是闭包, 被分发给不同的节点运行, 所以这个闭包中累加的并不是 Driver 中的这个变量
全局累加器
Accumulators(累加器) 是一个只支持 added
(添加) 的分布式变量, 可以在分布式环境下保持一致性, 并且能够做到高效的并发.
原生 Spark 支持数值型的累加器, 可以用于实现计数或者求和, 开发者也可以使用自定义累加器以实现更高级的需求
val config = new SparkConf().setAppName("ip_ana").setMaster("local[6]")
val sc = new SparkContext(config)
val counter = sc.longAccumulator("counter")
sc.parallelize(Seq(1, 2, 3, 4, 5))
.foreach(counter.add(_))
// 运行结果: 15
println(counter.value)
注意点:
Accumulator 是支持并发并行的, 在任何地方都可以通过
add
来修改数值, 无论是 Driver 还是 Executor只能在 Driver 中才能调用
value
来获取数值
在 WebUI 中关于 Job 部分也可以看到 Accumulator 的信息, 以及其运行的情况
累计器件还有两个小特性, 第一, 累加器能保证在 Spark 任务出现问题被重启的时候不会出现重复计算. 第二, 累加器只有在 Action 执行的时候才会被触发.
val config = new SparkConf().setAppName("ip_ana").setMaster("local[6]")
val sc = new SparkContext(config)
val counter = sc.longAccumulator("counter")
sc.parallelize(Seq(1, 2, 3, 4, 5))
.map(counter.add(_)) // 这个地方不是 Action, 而是一个 Transformation
// 运行结果是 0
println(counter.value)
自定义累加器
开发者可以通过自定义累加器来实现更多类型的累加器, 累加器的作用远远不只是累加, 比如可以实现一个累加器, 用于向里面添加一些运行信息
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
import org.junit.Test
class Accumulator {
@Test
def acc() : Unit = {
val config = new SparkConf().setMaster("local[6]").setAppName("acc")
val sc = new SparkContext(config)
val numAcc = new NumAccumulator()
//注册给spark
sc.register(numAcc,"num")
sc.parallelize(Seq("1","2","3")).foreach(x => numAcc.add(x))
println(numAcc.value)
}
}
/**
* 自定义累加器 实现 String -> Set[String]
*/
class NumAccumulator extends AccumulatorV2[String,Set[String]]{
//需要声明可变的Set集合
private val nums : scala.collection.mutable.Set[String] = scala.collection.mutable.Set()
/**
* 告诉Spark对象,这个累加器对象是否为空的
* @return
*/
override def isZero: Boolean = {
nums.isEmpty
}
/**
* 提供给Spark框架一个拷贝的累加器
* @return
*/
override def copy(): AccumulatorV2[String, Set[String]] = {
val newAccumulator = new NumAccumulator()
//这里需要保证线程安全
nums.synchronized{
newAccumulator.nums ++= this.nums
}
newAccumulator
}
/**
* 帮助Spark框架,清理累加器的内容
*/
override def reset(): Unit = {
nums.clear()
}
/**
* 外部传入的累加对象,在这个方法进行累加
* @param v
*/
override def add(v: String): Unit = {
nums += v
}
/**
* 累加器在进行累加的时候,可能每个分布式节点都有一个实例
* 在最后Driver进行一次合并,把所有实例的内容合并起来,会调用这个merge方法进行合并
* @param other
*/
override def merge(other: AccumulatorV2[String, Set[String]]): Unit = {
nums ++= other.value
}
/**
* 提供给外部累加结果
* 为什么一定要是不可变的,因为外部有可能再进行修改,如果有可变的集合,其外部的修改会影响内部的值
* @return
*/
override def value: Set[String] = {
nums.toSet
}
}
注意点:
可以通过继承
AccumulatorV2
来创建新的累加器有几个方法需要重写
reset 方法用于把累加器重置为 0
add 方法用于把其它值添加到累加器中
merge 方法用于指定如何合并其他的累加器
value
需要返回一个不可变的集合, 因为不能因为外部的修改而影响自身的值