源码
- groupByKey
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] = self.withScope {
//定义聚合方法
//前两个方法都是在map side执行的
//将每组内的第一个Value放到CompactBuffer容器内
val createCombiner = (v: V) => CompactBuffer(v)
//将每组内的2-n个元素加入到CompactBuffer容器内
val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v
//第三个函数 是在全局执行的,是在shuffledRead之后执行的
//分区间合并相同的CompactBuffer
val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false)
bufs.asInstanceOf[RDD[(K, Iterable[V])]]
}
此时我们看到底层调用的是 combineByKeyWithClassTag,传入3个函数
- combineByKeyWithClassTag
def combineByKeyWithClassTag[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
if (keyClass.isArray) {
if (mapSideCombine) {
throw new SparkException("Cannot use map-side combining with array keys.")
}
if (partitioner.isInstanceOf[HashPartitioner]) {
throw new SparkException("HashPartitioner cannot partition array keys.")
}
}
val aggregator = new Aggregator[K, V, C](
self.context.clean(createCombiner),
self.context.clean(mergeValue),
self.context.clean(mergeCombiners))
// 判断父子分区器是否一致,一致则直接使用mapPartitions,此时不会产生宽依赖
if (self.partitioner == Some(partitioner)) {
self.mapPartitions(iter => {
val context = TaskContext.get()
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
}, preservesPartitioning = true)
}
// 父子分区器不一致,new ShuffledRDD
else{
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}
}
combinerbykey()要经历三个阶段
第一个阶段是第一条记录的处理,第二个阶段是第二条及之后的记录的处理,第三个阶段是合并之前溢写出来多个小文件的处理。
这也是combinerbykey()要传入三个函数的原因。
这个函数将三个函数封装进一个aggregator里面,在函数的最后,会 new 一个ShuffledRDD,
并调用setSerializer(),setAggregator(),setMapSideCombine()三个方法,
第一个方法是序列化,第二个方法是聚合操作,即执行传入的三个方法,第三个是设置map端聚合。
图解
前两个方法都是在map side执行的
createCombiner:创建一个Combiner,就是将每一个组内的第一个Value加到ArrayBuffer内
mergeValue:将组内其他的value依次遍历加到ArrayBuffer内
mergeCombiners :第三个函数 是在全局执行的,是在shuffledRead之后执行的
示例
val rdd1: RDD[String] = sc.parallelize(List(
"spark", "hadoop", "hive", "spark",
"spark", "flink", "hive", "spark",
"kafka", "kafka", "kafka", "kafka",
"hadoop", "flink", "hive", "flink"
), 4)
//todo 1.groupByKey用法
rdd1.map((_, 1)).groupByKey()
.mapPartitionsWithIndex((index, iter) => {
iter.map((index, _))
}).foreach(println)
用shuffleRdd实现GroupByKey
val rdd1: RDD[String] = sc.parallelize(List(
"spark", "hadoop", "hive", "spark",
"spark", "flink", "hive", "spark",
"kafka", "kafka", "kafka", "kafka",
"hadoop", "flink", "hive", "flink"
), 4)
//todo 使用ShuffledRdd去实现groupByKey
val workAndOne: RDD[(String, Int)] = rdd1.map((_, 1))
val shuffledRdd: ShuffledRDD[String, Int, ArrayBuffer[Int]] = new ShuffledRDD[String, Int, ArrayBuffer[Int]](
workAndOne, new HashPartitioner(workAndOne.getNumPartitions)
)//传入父RDD 和 分区器
shuffledRdd.setMapSideCombine(false)//关闭map端的预聚合
//定义聚合方法
//前两个方法都是在map side执行的
//创建一个Combiner:就是将每一个组内的第一个Value加到ArrayBuffer内
val createCombiner: Int => ArrayBuffer[Int] = (a:Int)=>ArrayBuffer(a)
//将组内其他的value依次遍历加到ArrayBuffer内
val mergeValue =(ab:ArrayBuffer[Int],c:Int)=>ab += c
//第三个函数 是在全局执行的,是在shuffledRead之后执行的
val mergeCombiners = (ab1:ArrayBuffer[Int],ab2:ArrayBuffer[Int])=>ab1++=ab2
shuffledRdd.setAggregator(new Aggregator[String,Int,ArrayBuffer[Int]](
createCombiner,
mergeValue,
mergeCombiners
)).mapPartitionsWithIndex((index,iter)=>{
iter.map((index,_))
}).foreach(println)
注意事项
GroupByKey操作可能非常昂贵。如果您正在分组以便对每个键执行聚合(例如求和或平均),则使用
aggregateByKey
或reduceByKey
将提供更好的性能。
注意:按照目前的实现,groupByKey 必须能够保存内存中任何键的所有键值对。如果一个键的值太多,造成数据倾斜,可能会导致OutOfMemoryError
。