想看这几个算子的区别,最直接了当的方法就是查看底层源码。
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)在同一分区,下面全部使用该例子
- 将首个传入的value转变结构:将 1 转变成 CompactBuffer(1);2 => CompactBuffer(2), 4 => CompactBuffer(4)
- 分区内的计算:3 => CompactBuffer(3) ;
需要说明的是,由于 mapSideCombine 被设定为 false,因此分区内不会存在预聚合操作。 - 将分区与分区之间进行合并: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
- 将首个传入的value转变结构:将 1 转变成 1;2 => 2;4 => 4
- 在每个分区内,由于mapSideCombine设置为true,因此分区间可以进行合并:(1,3) => 4,3 => 3,4 => 4
- 将分区与分区之间进行合并: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) 为例
- 将首个传入的value转变结构:10;
- 分区内的计算:10 + 1 + 3 => 14,10 + 2 => 12,10 + 4 => 14;
- 将分区与分区之间进行合并: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)