groupBykey、reduceByKey、aggregateByKey、foldByKey和combineByKey的区别

445 阅读5分钟

想看这几个算子的区别,最直接了当的方法就是查看底层源码。

groupByKey:

def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] = self.withScope {
    // groupByKey shouldn't use map side combine because map side combine does not
    // reduce the amount of data shuffled and requires all map side data be inserted
    // into a hash table, leading to more objects in the old gen.
    val createCombiner = (v: V) => CompactBuffer(v)
    val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v
    val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
    val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
    createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false)
}

reduceByKey:

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
  combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}

aggregateByKey:

def aggregateByKey[U: ClassTag](zeroValue: U, partitioner: Partitioner)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
  val createZero = () => cachedSerializer.deserialize[U](ByteBuffer.wrap(zeroArray))

  // We will clean the combiner closure later in `combineByKey`
  val cleanedSeqOp = self.context.clean(seqOp)
  
  combineByKeyWithClassTag[U]((v: V) => cleanedSeqOp(createZero(), v),
    cleanedSeqOp, combOp, partitioner)
}

foldByKey:

def foldByKey(zeroValue: V, partitioner: Partitioner)(func: (V, V) => V): RDD[(K, V)] = self.withScope {
  ……
  val cleanedFunc = self.context.clean(func)
  combineByKeyWithClassTag[V]((v: V) => cleanedFunc(createZero(), v),
    cleanedFunc, cleanedFunc, partitioner)
}

combineByKey

def combineByKey[C](
    createCombiner: V => C,
    mergeValue: (C, V) => C,
    mergeCombiners: (C, C) => C,
    partitioner: Partitioner,
    mapSideCombine: Boolean = true,
    serializer: Serializer = null): RDD[(K, C)] = self.withScope {
  combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners,
    partitioner, mapSideCombine, serializer)(null)
}

很明显,这些方法内部都使用了combineByKeyWithClassTag方法,因此需要先查看该方法,再继续探究不同的算子是如何使用该方法的。

combineByKeyWithClassTag:

def combineByKeyWithClassTag[C](
    createCombiner: V => C,
    mergeValue: (C, V) => C,
    mergeCombiners: (C, C) => C,
    partitioner: Partitioner,
    mapSideCombine: Boolean = true,
    serializer: Serializer = null)

该方法需要六个参数:

  • createCombiner:可以理解为为了方便后续计算,将每个分区的第一个值转换结构;
  • mergeValue:分区内的计算规则;
  • mergeCombiners:分区间的计算规则;
  • partitioner:分区器;
  • mapSideCombine:是否在 map 端进行预聚合;
  • serializer:序列化器,默认为null。

文字描述显得过于抽象,下面根据具体函数来说明:

groupByKey:
将上面的源码赋值到combineByKeyWithClassTag方法中:

  • createCombiner:(v: V) => CompactBuffer(v)
  • mergeValue:(buf: CompactBuffer[V], v: V) => buf += v
  • mergeCombiners:(c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
  • partitioner:对key分区,默认使用的是HashPartitioner
  • mapSideCombine:mapSideCombine = false
  • serializer:序列化器未传参,默认是null

例:('a',1),('b',2),('a',3),('c',4) // ('a',1)和('a',3)在同一分区,下面全部使用该例子

  1. 将首个传入的value转变结构:将 1 转变成 CompactBuffer(1);2 => CompactBuffer(2), 4 => CompactBuffer(4)
  2. 分区内的计算:3 => CompactBuffer(3) ;
    需要说明的是,由于 mapSideCombine 被设定为 false,因此分区内不会存在预聚合操作。
  3. 将分区与分区之间进行合并:CompactBuffer(1,3),CompactBuffer(2),CompactBuffer(4)

得到最终结果。

reduceByKey:
由于reduceByKey需要传入一个参数,这里我们以 (a, b) => (a + b) 为例:

将上面的源码赋值到combineByKeyWithClassTag方法中:

  • createCombiner:(v: V) => v
  • mergeValue:(a, b) => (a + b)
  • mergeCombiners:(a, b) => (a + b)
  • partitioner:HashPartitioner
  • mapSideCombine:未传参,默认为true
  • serializer:序列化器未传参,默认是null
  1. 将首个传入的value转变结构:将 1 转变成 1;2 => 2;4 => 4
  2. 在每个分区内,由于mapSideCombine设置为true,因此分区间可以进行合并:(1,3) => 4,3 => 3,4 => 4
  3. 将分区与分区之间进行合并:4,3,4

得到最终结果。

需要注意的是,由于reduceByKey的参数为:func: (V, V) => V,因此reduceByKey算子无法对不同类型的Value进行聚合,否则会报错。

aggregateByKey:
将上面的源码赋值到combineByKeyWithClassTag方法中:

  • createCombiner:(v: V) => cleanedSeqOp(createZero(), v) // 这里进行了很复杂的操作,本质上就是createZero()
  • mergeValue:(U, V) => U
  • mergeCombiners:(U, U) => U
  • partitioner:对key分区,默认使用的是HashPartitioner
  • mapSideCombine:mapSideCombine = true
  • serializer:序列化器未传参,默认是null

由于aggregateByKey 需要传入三个参数,这里以 wordRDD.aggregateByKey(10)((a, b) => a + b, (a, b) => a + b) 为例

  1. 将首个传入的value转变结构:10;
  2. 分区内的计算:10 + 1 + 3 => 14,10 + 2 => 12,10 + 4 => 14;
  3. 将分区与分区之间进行合并:14,12,14

得到最终结果。

注意,由于我们已经限定了相同key在一个分区,假定代码如下:

val wordRDD: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("d", 4)), 2)

最总结果将所有不同,a为什么是24?,为什么 10 会加两次?

初始值会对每个分区中的数据进行叠加,当('a',1)和('a',3)不在同一分区时,10会分别和各自的 v 相加,也就是分区内相加,最后分区间叠加时会累计。

flodByKey:
将上面的源码赋值到combineByKeyWithClassTag方法中:

  • createCombiner:(v: V) => cleanedSeqOp(createZero(), v) // 这里进行了很复杂的操作,本质上就是createZero()
  • mergeValue:(V, V) => V
  • mergeCombiners:(V, V) => V
  • partitioner:对key分区,默认使用的是HashPartitioner
  • mapSideCombine:mapSideCombine = true
  • serializer:序列化器未传参,默认是null

这里已经非常明显,分区内和分区间的规则一致,因此就是 aggregateByKey 的简化,这里不再赘述。

combineByKey:
将上面的源码赋值到 combineByKeyWithClassTag 方法中:

  • createCombiner:V => C
  • mergeValue:(C, V) => C
  • mergeCombiners:(C, C) => C
  • partitioner:对key分区,默认使用的是HashPartitioner
  • mapSideCombine:mapSideCombine = true
  • serializer:序列化器未传参,默认是null

由此可见,combineByKey 是这几个bykey算子之间最通用的聚合算子,初始值,初始值、分区内计算规则、分区间计算规则等参数均可自定义。

示例:

object Practice {
  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("combineByKey")
    val sc = new SparkContext(sparkConf)

    val wordRDD: RDD[(String, Int)] = sc.makeRDD(List(
      ("a", 88), ("b", 95), ("a", 91),
      ("b", 93), ("a", 95), ("b", 98)
    ), 2)

    // 求每个key的平均值
    wordRDD.combineByKey(
      num => (num, 1), // 两个分区,每个分区取出第一个值转换结构:(88, 1)
      (a: (Int, Int), b) => {  // 分区内计算规则:(88, 1), 91 => (179, 2)
        (a._1 + b, a._2 + 1)
      },
      (a: (Int, Int), b: (Int, Int)) => {  // 分区间计算规则:(179, 2), (95, 1) => (274, 3)
        (a._1 + b._1 , a._2 + b._2)
      }
    )
    .mapValues(t => t._1 / t._2)  // (274, 3) => 274 / 3 = 91
    .collect().foreach(println)
    sc.stop()
  }

}

说明: 其实也可以用 aggregateByKey 来做,但是从源码中可以看出,aggregateByKey在使用时对第一个参数的类型 V 有要求,分区内的计算规则和分区间的计算规则都和该类型有关。因此不建议使用aggregateByKey。

这里附上一段aggregateByKey的写法:

wordRDD.aggregateByKey((0, 0))(
  (x, y: Int) => (x._1 + y, x._2 + 1),
  (x: (Int, Int), y: (Int, Int)) => (x._1 + y._1, x._2 + y._2)
).mapValues(t => t._1 / t._2).collect().foreach(println)