Spark Accumulator
累加器作用
- 累加器:分布式只写变量(Executor端的task不能互相访问累加器的值)。
- 累加器对信息进行聚合。
源码
累加器的基类,可以累加 IN 类型的输入,并产生 OUT 类型的输出。
OUT 应该是可以原子读取的类型(例如,Int、Long)或线程安全的(例如,synchronized collections),因为它将从其他线程读取。
abstract class AccumulatorV2[IN, OUT] extends Serializable {
...
}
累加器原理图
Spark中累加器的执行流程:
- 1.首先序列化 driver 端 accumulator 到 executor ,序列化前调用 reset 重置 value 并使用 isZero 检测是否重置成
- 2.有几个task,spark engine就调用copy方法拷贝几个累加器(不注册的)
- 3.单个 executor 内使用 add 进行累加(注意在此过程中,被最初注册的累加器的值是不变的),
- 4.最终 driver 端对多个 executor 间的 accumulaotr 使用merge 进行合并得到结果。
累加器使用demo
- demo1
package test
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.util.AccumulatorV2
import scala.collection.mutable
object AccumulatorTest {
def main(args: Array[String]): Unit = {
val session: SparkSession = SparkSession.builder().appName(this.getClass.getSimpleName)
.master("local[6]").getOrCreate()
val sc: SparkContext = session.sparkContext
val rdd2: RDD[(String, String)] = sc.makeRDD(List(("home", "北京"),("name", "小宽"), ("age", "30"), ("tools", "篮球"), ("sex", "男")), 4)
//注册累加器
val myAcc2: MyAccumulator = new MyAccumulator
sc.register(myAcc2,"myAcc2")
println(rdd2.getNumPartitions)
// rdd2.mapPartitions(iter=>{
// iter.map(f=>{
// myAcc2.add(f._1,f._2)
// })
// }) //map算子累加器不加
rdd2.foreachPartition(iter=>{
iter.foreach(f=>{
myAcc2.add(f._1,f._2)
})
})
myAcc2.value.foreach(println)
while (true){}
session.close()
}
}
//(String,String) -> 输入数据格式 , mutable.Map[String,AnyRef]-> 输出格式
class MyAccumulator extends AccumulatorV2[(String,String),mutable.Map[String,AnyRef]] {
//声明一个Map 存放最终结果
private val map: mutable.Map[String, AnyRef] = mutable.Map[String, AnyRef]()
override def reset(): Unit = map.clear()
override def isZero: Boolean = map.isEmpty
//driver copy累加器到executor
override def copy(): AccumulatorV2[(String, String), mutable.Map[String, AnyRef]] = new MyAccumulator
//executor 内部累加
override def add(v: (String, String)): Unit = map.put(v._1,v._2)
//累加器在driver端聚合
override def merge(other: AccumulatorV2[(String, String), mutable.Map[String, AnyRef]]): Unit = {
val thisMap: mutable.Map[String, AnyRef] = this.map //driver端的map
val otherMap: mutable.Map[String, AnyRef] = other.value //executor返回来的计算结果
thisMap ++= otherMap
}
//返回累加器
override def value: mutable.Map[String, AnyRef] = map
}
spark ui
- demo2 WC
val rdd2 = sc.makeRDD(List("spark","flink","java","scala","java"))
val myAcc2: MyAccumulator2 = new MyAccumulator2
sc.register(myAcc2,"myAcc2")
rdd2.foreachPartition(iter=>{
iter.foreach(f=>{
myAcc2.add(f)
})
})
// 自定义累加器
class MyAccumulator2 extends AccumulatorV2[String,mutable.Map[String,Long]] {
//声明一个Map 存放最终结果
private val map = mutable.Map[String, Long]()
override def reset(): Unit = map.clear()
override def isZero: Boolean = map.isEmpty
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = new MyAccumulator2
override def add(v: String): Unit = {
val l: Long = map.getOrElse(v, 0L) +1
map.update(v,l)
}
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
val map1: mutable.Map[String, Long] = this.map
val map2: mutable.Map[String, Long] = other.value
map2.foreach{
case (k,v)=>{
val l: Long = map1.getOrElse(k,0L) + v
map1.update(k,l)
}
}
}
override def value: mutable.Map[String, Long] = this.map
}
输出:
//(spark,1)
//(scala,1)
//(flink,1)
//(java,2)
使用累加器中可能遇到的坑
当我们把累加器的操作放在 map 中执行的时候,后续如果有多个 action 操作共用该累加器的 RDD ,将会导致重复执行。也就意味着累加器会重复累加。为了避免这种错误,我们最好只在 action 算子如 foreach 中使用累加器,如果实在需要在 transformation 中使用,记得使用 cache 操作
1.累加器少加
//注册累加器
val myAcc: LongAccumulator = sc.longAccumulator("myAcc")
//todo 1.累加器少加 : 累计器是lazy加载的 没action算子 不执行
sc.parallelize(1 to 20).map(myAcc.add(_))
println(myAcc.value) // 0
2.累加器多加
//todo 2.累加器多加: 多个action算子 重复执行
val rdd: RDD[Int] = sc.parallelize(1 to 10).map(f => {
myAcc.add(1)
f + 1
})
rdd.count()
println("AccumuLator1: " + myAcc.value)
rdd.reduce(_+_)
println("AccumuLator2: " + myAcc.value)
//AccumuLator1: 10
//AccumuLator2: 20
map算子实际上被执行了两次,在reduce操作提交作业后累加器又完成了一轮技数,所以最终的累加器的值为20。究其原因是因为count虽然促使numberRDD累计出来,但是由于没有对其进行缓存,所以下次再次需要使用numberRDD这个数据集时,还需要从并行化数据集的部分开始执行计算
2.1避免累加器多加
//2.1 cache
//在count之前调用rdd的cache方法(或persist),这样在count后数据集就会被缓存下来
//reduce操作就会读取缓存的数据集,而无需从头开始计算。
val rdd: RDD[Int] = sc.parallelize(1 to 10).map(f => {
myAcc.add(1)
f + 1
})
rdd.cache()
rdd.count()
println("AccumuLator1: " + myAcc.value)
rdd.reduce(_+_)
println("AccumuLator2: " + myAcc.value)
//AccumuLator1: 10
//AccumuLator2: 10
//todo 2.2. 如果累计器在actions操作算子里面执行时,只会累加一次
val rdd2: RDD[Int] = sc.parallelize(1 to 10)
rdd2.foreach(f=>{
myAcc.add(1)
f+1
})
println("AccumuLator1: " + myAcc.value)
rdd2.count()
println("AccumuLator2: " + myAcc.value)
//AccumuLator1: 10
//AccumuLator2: 10