闭包
闭包的作用可以理解为:函数可以访问函数外部定义的变量,但是函数内部对该变量进行的修改,在函数外是不可见的,即对函数外源变量不会产生影响。
//闭包示例
@Test
def test(): Unit = {
val areaFunction = closure()
val area = areaFunction(2)
println(area)
}
def closure(): Int => Double = {
val factor = 3.14
val areaFunction = (r: Int) => math.pow(r, 2) * factor
areaFunction
}
上述例子中, closure
方法返回的一个函数的引用, 其实就是一个闭包, 闭包本质上就是一个封闭的作用域, 要理解闭包, 是一定要和作用域联系起来的.
Spark闭包
首先通过下边对RDD中的元素进行求和的示例,程序打印的结果不是15,而是0:
val data = Array(1, 2, 3, 4, 5)
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
// rdd.foreach(x => counter += x)
rdd.foreach(x => {
counter += x
println("foreach: " + counter)
})
println("Counter value: " + counter)
// 运行结果
foreach: 2
foreach: 3
foreach: 4
foreach: 1
foreach: 5
Counter value: 0
Spark为了执行任务,会将RDD的操作分解为多个task,并且这些task是由executor执行的。在执行之前,Spark会计算task的闭包即定义的一些变量和方法,比如例子中的counter变量和foreach方法,并且闭包必须对executor而言是可见的,这些闭包会被序列化
发送到每个executor。在集群模式下,driver和executor运行在不同的JVM进程中,发送给每个executor的闭包中的变量是driver端变量的副本
。因此,当foreach函数内引用counter时,其实处理的只是driver端变量的副本,与driver端本身的counter无关。driver节点的内存中仍有一个计数器,但该变量对executor是不可见的!executor只能看到序列化闭包的副本。因此,上述例子输出的counter最终值仍然为零,因为counter上的所有操作都只是引用了序列化闭包内的值。在本地模式下,往往driver和executor运行在同一JVM进程中。那么这些闭包将会被共享,executor操作的counter和driver持有的counter是同一个,那么counter在处理后最终值为15。
但是在生产中,我们的任务都是在集群模式下运行,如何能满足这种业务场景呢?Accumulator即累加器
。Spark中的累加器专门用于提供一种机制,用于在集群中的各个worker节点之间执行时安全地更新变量。
一般来说,closures - constructs比如循环或本地定义的方法,就不应该被用来改变一些全局状态,Spark并没有定义或保证对从闭包外引用的对象进行更新的行为。如果你这样操作只会导致一些代码在本地模式下能够达到预期的效果,但是在分布式环境下却事与愿违。如果需要某些全局聚合,请改用累加器。对于其他的业务场景,我们适时考虑引入外部存储系统、广播变量等。