共享变量之累加器

Spark 累加器:Driver 的变量在 Executor 上改了没用?用累加器把数”收”回来

在 Spark 里,你可能会写出这样的代码:

var sum = 0
rdd.foreach(x => sum += x)
println(sum)   // 输出:0

直觉上应该是所有元素的和,但结果是 0。因为 sum 在 Driver 端定义,被复制到每个 Executor 上各自修改,改的是副本,Driver 端的 sum 纹丝不动。

累加器就是解决这个问题的:Driver 发一个”计数器”给 Executor,Executor 往里加数,Driver 最后把总数收回来。

累加器就干一件事:把 Executor 上的值聚合回 Driver。

累加器的基本用法

1. 创建累加器

val sumAcc = sc.longAccumulator("Sum")   // 长整型累加器
val countAcc = sc.doubleAccumulator("Count")  // 双精度累加器
val errAcc = sc.collectionAccumulator[String]("Errors")  // 集合累加器

2. Executor 上累加

rdd.foreach(x => sumAcc.add(x))   // 每个 Task 往累加器里加数

3. Driver 读取结果

println(sumAcc.value)   // 所有 Task 累加的总和

完整示例:

val rdd = sc.parallelize(1 to 100)
val sumAcc = sc.longAccumulator("Sum")

rdd.foreach(x => sumAcc.add(x))

println(sumAcc.value)   // 5050

累加器的类型

Spark 内置三种累加器:

类型 创建方法 用途
长整型 sc.longAccumulator("name") 计数、求和(int/long)
双精度 sc.doubleAccumulator("name") 求和(float/double)
集合 sc.collectionAccumulator[T]("name") 收集错误信息、特殊值

集合累加器最实用: 收集处理过程中遇到的异常数据。

val errorAcc = sc.collectionAccumulator[String]("Errors")

rdd.foreach { record =>
  if (record.contains("ERROR")) {
    errorAcc.add(record)
  }
}

errorAcc.value.forEach(println)   // 打印所有错误记录

自定义累加器

内置的不够用,自己写一个。继承 AccumulatorV2,实现 6 个方法:

import org.apache.spark.util.AccumulatorV2

// 字符串拼接累加器
class StringConcatAccumulator extends AccumulatorV2[String, String] {
  private var result: String = ""

  override def isZero: Boolean = result.isEmpty
  override def copy(): AccumulatorV2[String, String] = {
    val acc = new StringConcatAccumulator
    acc.result = this.result
    acc
  }
  override def reset(): Unit = result = ""
  override def add(v: String): Unit = result += v
  override def merge(other: AccumulatorV2[String, String]): Unit = {
    result += other.value
  }
  override def value: String = result
}

// 使用
val acc = new StringConcatAccumulator()
sc.register(acc, "StringConcat")
rdd.foreach(x => acc.add(x.toString + ","))
println(acc.value)   // "1,2,3,4,5,"

关键注意点:转换算子 vs 行动算子

累加器只在行动算子(Action)里可靠。 在转换算子(Transformation)里用累加器,可能因为惰性执行导致重复累加。

//  错误:在 map 里累加
val acc = sc.longAccumulator
val mapped = rdd.map(x => {
  acc.add(1)
  x * 2
})
mapped.count()   // 第一次:累加 100
mapped.count()   // 第二次:又累加 100  ← 重复了!
// 因为 map 被 count 触发了两次

//  正确:在 foreach 里累加
rdd.foreach(x => acc.add(1))  // 只执行一次

规则:累加器只放在 Action 里(foreachcollectcount),别放在 Transformation 里(mapfilter)。

常见问题

1. 累加器值不对,比预期大

多半是累加器放在了 Transformation 里,被重复执行了。检查是不是在 map/filter 里用了累加器。

2. Executor 上想读累加器当前值,读不到

累加器是只写的,Executor 上只能 add,不能 value。想读值只能回到 Driver 读 acc.value

3. 累加器更新慢

数据量大、分区多的时候,每个 Task 更新累加器都要序列化回 Driver。如果只是计数,可以考虑在每个分区先局部计数,最后汇总。