spark难解的地方之一就是明白在集群上运行代码时,明白变量和函数的作用范围和生命周期,在作用域外修改变量的RDD操作很多时候会产生麻烦。接下来展示的例子我们会使用foreach() 来增加变量counter,相类似的问题也可能同样出现在其他loop问题中.
例子:
对于RDD中元素(element)求和。即使使用相同的JVM代码,由于不同的操作环境,操作的结果也可能不同,常用的例子是用Spark 在当地环境跑(--master = local[n])和在集群上(spark-submmit to YARN)
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
上面这段代码的结果是不明确和稳定的,可能不符合我们的预期.为了运行任务(jobs),Spark会将RDD操作过程分解成tasks,每个task由一个执行器执行。不过在执行前,Spark会计算所有task的闭包,闭包中包含了对于每个执行器可见的变量和函数(在这里是foreach()),闭包被序列化后发送给每个执行器.
闭包里面的变量被复制发送到每个执行器中,在foreach()函数中引用的counter变量不再是驱动节点(driver node)上的counter变量,当然在驱动节点的内存上还是存在counter变量,不过这个counter变量对于每个工作的执行器来说是不可见的,执行器只可以看见来自序列化闭包的复制变量,由于在counter变量上所做的所有操作都是引用的序列化闭包中数据,因此,counter最后的值还是为0。
在当地模式中,在某些环境下,foreach()函数将会使用与驱动节点相同的JVM来运行,将会使用相同原始变量counter,所以可以进行更新.
为了保证我们累加的结果是明确的,我们应该使用Accumulator.Spark中的Accumulator特别适合处理集群计算环境下(运行分解为多个worker nodes)对变量进行更新。
比如像 loops 或者 局部定义方法(locally defined methods)这种闭包结构,不应该用来改变全局变量,在这个例子中,我们使用foreach(x => +=x)来迭代增加全局变量。
Spark不能保证闭包外对象的变动,比如在我们这个例子中闭包中的变量counter发生改动,在闭包外引用这个变量,其结果不会发生改变。可能有些类似的代码在本地会有效,不过这仅仅是侥幸,在分布式运行中,将不会达到我们的预期。对于全局变量累计的问题,应该转而使用Accumularor。