摘要 :Spark 1.x 的
Accumulator接口存在严重设计缺陷:类型单调(IN=OUT)、无法自定义扩展、内置实现仅 4 种。Spark 2.0 引入了AccumulatorV2[IN,OUT]重构了整个累加器体系,通过 7 个核心抽象方法(add / merge / copy / copyAndReset / reset / isZero / value)实现了完全可扩展的累加器框架。本文从 Accumulator → AccumulatorV2 接口演化、自定义累加器实现模式、Executor 端 Task 隔离机制、Spark 1.x → 2.x → 3.x 版本对比变化四个维度,配合 2 张原创架构图 + 代码实例,带你彻底掌握自定义累加器设计与版本迁移。关键词:AccumulatorV2, 自定义累加器, Spark 版本演进, copyAndReset, Task 隔离, Spark 1.x vs 2.x vs 3.x, merge 合并律
一、开篇:从 Spark 1.x 的局限说起
如果你是从 Spark 1.x 时代走过来的工程师,下面这段代码一定不陌生:
scala
// Spark 1.x:老式累加器
val count = sc.accumulator(0, "recordCount")
rdd.foreach(_ => count += 1)
// 仅此而已------你想统计更多维度?做不到。
Spark 1.x 累加器的三大硬伤:
| 痛点 | 说明 | 后果 |
|---|---|---|
| ① 类型单调 | Accumulator[T] --- IN 和 OUT 必须是同一类型 |
无法做 Double→Stats 的类型转换 |
| ② 无法扩展 | 没有 merge/copy/reset 等抽象方法 | 自定义累加器几乎不可能 |
| ③ 内置稀少 | 仅 Int/Long/Double/Float | 集合类、统计类均无法原生支持 |
scala
// Spark 1.x 源码:Accumulator 简陋接口
class Accumulator[T] private[spark] (
@transient private[spark] val initialValue: T,
param: AccumulatorParam[T],
name: Option[String],
...
) extends Accumulable[T, T](initialValue, param, name, ...) {
// 没有 merge / copyAndReset / isZero 等扩展点!
}
Spark 2.0 彻底重构了这个局面------AccumulatorV2IN, OUT 横空出世。
二、架构全景:自定义累加器实现全流程

2.1 AccumulatorV2 七核心方法
scala
abstract class AccumulatorV2[IN, OUT] extends Serializable {
// ---- Executor 端 ----
def add(v: IN): Unit // ① Task 内部调用,本地累加
// ---- 合并操作 ----
def merge(other: AccumulatorV2[IN, OUT]): Unit // ② Driver 端合并 Task 副本
def copy(): AccumulatorV2[IN, OUT] // ③ 深拷贝(保留当前值)
def copyAndReset(): AccumulatorV2[IN, OUT] // ④ 拷贝并置零 → 给新 Task
// ---- 查询与重置 ----
def reset(): Unit // ⑤ 重置为零值
def isZero: Boolean // ⑥ 是否为零值(用于验证 reset 后状态)
def value: OUT // ⑦ 读取累加值(单向:仅 Driver 有意义)
}
方法之间的不变量:
scala
// 等式 1:copyAndReset 等价于 copy + reset
accum.copyAndReset() ≡ { val cp = accum.copy(); cp.reset(); cp }
// 等式 2:reset 后必须为 isZero
accum.reset(); assert(accum.isZero)
// 等式 3:merge 必须满足合并律(幂等)
a.merge(b); b.merge(a); assert(a.value == b.value)
2.2 自定义累加器实战:StatsAccumulator
scala
import org.apache.spark.util.AccumulatorV2
class StatsAccumulator extends AccumulatorV2[Double, (Double, Double, Double, Long)] {
// 内部状态:线程安全
private var _min: Double = Double.PositiveInfinity
private var _max: Double = Double.NegativeInfinity
private var _sum: Double = 0.0
private var _count: Long = 0L
// ① 零值判断
override def isZero: Boolean = _count == 0L
// ② 深拷贝(保值)
override def copy(): StatsAccumulator = {
val cp = new StatsAccumulator
cp._min = this._min
cp._max = this._max
cp._sum = this._sum
cp._count = this._count
cp
}
// ③ 重置为零
override def reset(): Unit = {
_min = Double.PositiveInfinity
_max = Double.NegativeInfinity
_sum = 0.0
_count = 0L
}
// ④ Task 端累加
override def add(v: Double): Unit = {
_min = math.min(_min, v)
_max = math.max(_max, v)
_sum += v
_count += 1
}
// ⑤ merge:Driver 端合并 Task 副本(幂等!)
override def merge(other: AccumulatorV2[Double, _]): Unit = {
other match {
case o: StatsAccumulator =>
_min = math.min(_min, o._min)
_max = math.max(_max, o._max)
_sum += o._sum
_count += o._count
case _ =>
throw new UnsupportedOperationException(
s"Cannot merge ${other.getClass.getName} with StatsAccumulator")
}
}
// ⑥ 返回最终统计
override def value: (Double, Double, Double, Long) = (_min, _max, _sum, _count)
}
注册与使用:
scala
val statsAcc = new StatsAccumulator
sc.register(statsAcc, "dataStats") // ⚡ register → Spark UI 可追踪!
rdd.foreach { record =>
statsAcc.add(record.value)
}
// Action 完成后读取
val (minVal, maxVal, sumVal, cnt) = statsAcc.value
println(s"[Stats] Min=$minVal Max=$maxVal Avg=${sumVal/cnt} Count=$cnt")
三、核心机制:Task 隔离与副本回传
3.1 copyAndReset 的意义
copyAndReset() 是整个累加器框架中最巧妙的设计。它解决了"如何在 Executor 端给每个 Task 一个独立的零值副本"这个经典问题。
scala
// 时序流程
Driver: val acc = new StatsAccumulator // ① 元对象(id=5, 零值)
: sc.register(acc, "myStats") // ② 注册到 AccumulatorContext
DAGScheduler.submitMissingTasks():
→ 序列化 Task
→ accMerged = acc.copyAndReset() // ③ 生成独立零值副本
→ Task 携带 accMerged(id=5, 零值)
Executor: 反序列化 Task → acc.add(3.14) // ④ 本地累加
: acc.add(2.72)
: Task 完成 → TaskResult(accUpdates) // ⑤ 回传累加结果
→ accumulatorUpdates = Map(5 → (3.14+2.72))
Driver: acc.merge(execCopy) // ⑥ Driver 元对象合并
: println(acc.value) // ⑦ 读取最终值
scala
// 源码验证:Task 序列化时调用 copyAndReset
// DAGScheduler.scala
val taskBinary = sc.broadcast(taskBinaryBytes)
new ShuffleMapTask(
stageId, stageAttemptId, taskBinary,
partition, locs, properties,
// 关键:每个 Task 获取累加器独立副本
serializedTaskMetrics,
Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
// ↓ 累加器通过 copyAndReset 隔离
stage.latestInfo.accumulables.values.map(_.copyAndReset()).toSeq
)
3.2 merge 的合并律要求
merge() 是累加器正确性的基石。由于 Task 回传是无序且可能重试的,merge 必须满足:
scala
// 规则 1:交换律
a.merge(b) ≡ b.merge(a)
// 规则 2:结合律
(a merge b) merge c ≡ a merge (b merge c)
// 规则 3:零值单位元
a merge zero == a
scala
// ❌ 错误示范:违反了交换律!
class BadAccumulator extends AccumulatorV2[String, String] {
private var _list = List.empty[String]
override def add(v: String): Unit = _list = _list :+ v // 顺序追加
override def merge(o: AccumulatorV2[String,String]): Unit = {
_list = _list ++ o.asInstanceOf[BadAccumulator]._list // 顺序依赖!
}
// ❌ merge 结果依赖于调用顺序,破坏了交换律
}
// ✅ 正确做法:使用 Set 或排序后合并
class GoodAccumulator extends AccumulatorV2[String, Set[String]] {
private var _set = Set.empty[String]
override def add(v: String): Unit = _set += v
override def merge(o: AccumulatorV2[String, Set[String]]): Unit = {
_set = _set ++ o.asInstanceOf[GoodAccumulator]._set // Set 并集 → 交换律成立
}
}
四、版本演进全景:Accumulator → AccumulatorV2

4.1 Spark 1.x Accumulator(已弃用)
scala
// Spark 1.x API --- @deprecated since 2.0.0
val counter = sc.accumulator(0, "myCounter") // Accumulator[Int]
val sum = sc.accumulator(0.0, "mySum") // Accumulator[Double]
rdd.foreachPartition { iter =>
iter.foreach { record =>
counter += 1 // += 操作符
sum += record.value
}
}
// 限制:
// - counter 只能是 Int → Int(无法改为 Int → Long)
// - 无法自定义 AccumulatorParam(接口不开放)
// - 无法实现 CollectionAccumulator / 多维度统计
4.2 Spark 2.x AccumulatorV2(当前主版本)
scala
// Spark 2.x/3.x API --- AccumulatorV2[IN, OUT]
val counter = sc.longAccumulator("counter") // LongAccumulator extends AccuV2
val doubleAcc = sc.doubleAccumulator("sum") // DoubleAccumulator
val collectAcc = sc.collectionAccumulator[String]("errors") // CollectionAccumulator
val customAcc = new StatsAccumulator // 自定义!
sc.register(customAcc, "stats") // register → UI 可见
// IN ≠ OUT 支持!
// LongAccumulator: IN=Long, OUT=Long
// StatsAccumulator: IN=Double, OUT=(min,max,sum,count)
// CollectionAccumulator: IN=String, OUT=java.util.List[String]
4.3 Spark 3.x 增强
| 版本 | 新增特性 | 说明 |
|---|---|---|
| 3.0 | register() API 稳定 |
自定义累加器注册到 AccumulatorContext |
| 3.1 | countFailedValues 标志 |
Task 失败/重试的累加器值追踪 |
| 3.2 | Spark UI Accumulators Tab | 实时可视化累加器值 |
| 3.3 | AverageAccumulator |
内置 (sum, count) 双维度累加器 |
4.4 完整版本对比表
scss
Spark 1.x Spark 2.x Spark 3.x
──────────── ──────────── ────────────
抽象基类 Accumulator[T] AccumulatorV2[IN,OUT] AccumulatorV2[IN,OUT]
类型系统 T → T 单调 IN/OUT 独立泛型 IN/OUT 独立泛型
内置实现 4 种 4 种 + CollectionAcc 4 种 + CollectionAcc + AvgAcc
自定义扩展 ❌ 不支持 ✅ extends AccuV2 ✅ 完全支持
核心方法 2 个 (add/value) 7 个 (含merge/copy) 7 个 + countFailedValues
创建方式 sc.accumulator(0) sc.longAccumulator() sc.register(custom,"name")
Spark UI 有限支持 Accumulators Tab 增强可视化 + 实时追踪
状态 @deprecated 活跃主版本 持续增强
五、自定义累加器实战:三种常见模式
5.1 模式一:SetAccumulator(去重计数)
scala
import scala.collection.mutable
import org.apache.spark.util.AccumulatorV2
class SetAccumulator[T] extends AccumulatorV2[T, mutable.Set[T]] {
private val _set: mutable.Set[T] = mutable.Set.empty
override def isZero: Boolean = _set.isEmpty
override def copy(): SetAccumulator[T] = {
val cp = new SetAccumulator[T]
cp._set ++= this._set
cp
}
override def reset(): Unit = _set.clear()
override def add(v: T): Unit = _set += v
override def merge(other: AccumulatorV2[T, mutable.Set[T]]): Unit = {
_set ++= other.asInstanceOf[SetAccumulator[T]]._set
}
override def value: mutable.Set[T] = _set
}
// 使用:统计所有访问过的用户 ID
val userSet = new SetAccumulator[String]
sc.register(userSet, "uniqueUsers")
logsRDD.foreach { log =>
userSet.add(log.userId)
}
println(s"独立用户数: ${userSet.value.size}")
5.2 模式二:HistogramAccumulator(分布统计)
scala
class HistogramAccumulator(buckets: Array[Double])
extends AccumulatorV2[Double, Map[String, Long]] {
private val _histogram = mutable.Map.empty[String, Long]
override def add(v: Double): Unit = {
val bucket = buckets.zipWithIndex
.find { case (bound, _) => v <= bound }
.map { case (_, i) => s"≤ ${buckets(i)}" }
.getOrElse("> ${buckets.last}")
_histogram(bucket) = _histogram.getOrElse(bucket, 0L) + 1
}
override def merge(other: AccumulatorV2[Double, Map[String, Long]]): Unit = {
other.value.foreach { case (k, v) =>
_histogram(k) = _histogram.getOrElse(k, 0L) + v
}
}
// ... isZero/copy/reset/value 实现略
}
5.3 模式三:BloomFilterAccumulator(布隆过滤器)
scala
class BloomFilterAccumulator(expectedInsertions: Long, fpp: Double)
extends AccumulatorV2[String, BloomFilter[String]] {
private var _filter: BloomFilter[String] =
BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp)
override def add(v: String): Unit = _filter.put(v)
override def merge(other: AccumulatorV2[String, BloomFilter[String]]): Unit = {
_filter.putAll(other.value)
}
// ... copy/reset/isZero/value 实现
// 使用
def mightContain(v: String): Boolean = _filter.mightContain(v)
}
六、迁移指南:1.x → 2.x/3.x
6.1 直接映射
scala
// ❌ Spark 1.x
val counter = sc.accumulator(0, "counter")
// ✅ Spark 2.x/3.x
val counter = sc.longAccumulator("counter")
scala
// ❌ Spark 1.x
val sum = sc.accumulator(0.0, "sum")
// ✅ Spark 2.x/3.x
val sum = sc.doubleAccumulator("sum")
6.2 自定义累加器迁移
scala
// ❌ Spark 1.x: 通过 AccumulableParam 变通(复杂且受限)
class StatsParam extends AccumulableParam[Stats, Double] {
def addAccumulator(stats: Stats, v: Double): Stats = { stats.add(v); stats }
def addInPlace(s1: Stats, s2: Stats): Stats = { s1.merge(s2); s1 }
def zero(initial: Stats): Stats = new Stats()
}
// ✅ Spark 2.x/3.x: 直接 extends AccumulatorV2
class StatsAccumulator extends AccumulatorV2[Double, (Double, Double, Double, Long)] {
// 清晰、直观、类型安全
}
6.3 register vs 便捷 API
scala
// 便捷 API(内置类型)
sc.longAccumulator("name") // → 自动注册到 AccumulatorContext
sc.doubleAccumulator("name") // → 同上
sc.collectionAccumulator[T]("name")
// 自定义累加器 → 必须手动 register
val custom = new MyCustomAccumulator
sc.register(custom, "myCustom") // → Spark UI 可见
// vs.
// val custom = new MyCustomAccumulator // ❌ 不 register → UI 不可见
七、总结
-
Spark 1.x Accumulator 设计简陋(IN=OUT、无 merge、内置仅 4 种),自 2.0 起弃用。迁移成本极低:
sc.accumulator(0)→sc.longAccumulator()。 -
AccumulatorV2IN,OUT 通过 7 个核心抽象方法实现了完全可扩展的累加器体系。关键设计:
copyAndReset实现 Task 隔离 +merge满足合并律实现 Driver 聚合。 -
自定义模式 覆盖了去重计数(SetAccumulator)、分布统计(HistogramAccumulator)、布隆过滤器(BloomFilterAccumulator)等多种场景,通过
sc.register()即可接入 Spark UI 追踪。 -
Spark 3.x 引入了
countFailedValues、AverageAccumulator、UI 增强等改进,持续完善累加器生态。
作者 :starzy
博客 :blog.starzy.cn
GitHub :starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践