从Spark官网闭包说起
Spark官网Understanding closures(闭包)部分指出,Spark的一个难点在于理解变量和方法的范围和生命周期。
//使用foreach()计算
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
以上代码将不会正确计算出counter值。Spark会将counter的副本发送到每个executor上,对counter副本的修改不会改变Driver端的counter,counter的值仍然是0。
在这种计数或求和的场景可以使用累加器(Accumulator),下面将详细说明。
参考:http://spark.apache.org/docs/latest/rdd-programming-guide.html#understanding-closures-
共享变量
Spark计算传递的函数是在远程节点上执行的,函数中使用的是变量的副本。这些变量被复制到每台机器上,变量的更新不会返回到Driver端。
示例图
Spark程序计算的时候,函数是分布到每一个节点去执行,所有的变量副本会传给节点计算,但是计算完成后,变量的更新不会返回到driver端。如下图:
代码示例:
val rdd = sc. textFile("README.md") //下文中rdd默认使用此值,README.md为Spark源码中的文档
val count = rdd.count()
var i = 0
val mapRDD = rdd.map(line => {
i = i + 1
(line,line.size)
})
println("i:" + i)
println("count:" + count)
i:0
count:103
对于跨集群跨任务的变量共享,Spark提供了两种方式:广播变量和累加器。
广播变量(针对只读变量)Broadcast Variables
核心:将只读变量缓存在每个worker节点上。
一:每个节点只要传输一次给Executor,可以给多个Task使用。
二:节点上的变量副本并不都来自与Driver,而是采用了高效广播算法(TorrentBroadcast)降低通信成本。
广播变量有什么作用?
用作Join时的调优map-side join
Join操作 ——》 Shuffle操作 ——》 性能问题
如果Join两边的RDD,有一个RDD的数量较小,可以将小的RDD数据广播出去,将Join转化为map-side join
注意事项: RDD是不能直接广播的。要广播数据,而RDD要遇到Action操作才会执行。
直接广播RDD会提示"Can not directly broadcast RDDs; instead, call collect() and broadcast the result."
val rddData = rdd.collectAsMap()
val rddBC = sc.broadcast(rddData)
val rdd3 = rdd2.mapPartitions(partition => {
val bc = rddBC.value
for{
(key,value) <- partition
if(rddData.contains(key))
}yield(key, (bc.get(key).getOrElse(""),value))
})
累加器(只能在Driver端获取)Accumulator
累加器用于Spark任务的计数和求和场景。
累加器只能累加,获取只能在driver端,节点是不知道累加器的值的。
累加器的使用需谨慎,如以下示例:
val acc = sc.longAccumulator("counterAcc")
val mapRDD = rdd.map(line => {
acc.add(1)
(line, line.size)
})
val count = mapRDD.count()
val count2 = mapRDD.count()
println("acc.value)
mapRDD.count()两次Action会导致累加器的值翻倍。上述情况使用缓存可以防止acc.value的值翻倍。但实际情况中不推荐使用累加器。
- 执行多个action会出错。
- 虽然可以缓存,但是缓存可能被清除。
- 累加器仅用于程序调试。