下面來看看groupByKey和reduceByKey的區別:
val conf = new SparkConf().setAppName("GroupAndReduce").setMaster("local")
val sc = new SparkContext(conf)
val words = Array("one", "two", "two", "three", "three", "three")
val wordsRDD = sc.parallelize(words).map(word => (word, 1))
val wordsCountWithReduce = wordsRDD.
reduceByKey(_ + _).
collect().
foreach(println)
val wordsCountWithGroup = wordsRDD.
groupByKey().
map(w => (w._1, w._2.sum)).
collect().
foreach(println)
雖然兩個函數都能得出正確的結果, 但reduceByKey函數更適合使用在大數據集上。 這是因為Spark知道它可以在每個分區移動數據之前將輸出數據與一個共用的key
結合。
借助下圖可以理解在reduceByKey里發生了什么。 在數據對被搬移前,同一機器上同樣的key
是怎樣被組合的( reduceByKey中的 lamdba 函數)。然后 lamdba 函數在每個分區上被再次調用來將所有值 reduce成最終結果。整個過程如下:

另一方面,當調用 groupByKey時,所有的鍵值對(key-value pair) 都會被移動,在網絡上傳輸這些數據非常沒必要,因此避免使用 GroupByKey。
為了確定將數據對移到哪個主機,Spark會對數據對的key
調用一個分區算法。 當移動的數據量大于單臺執行機器內存總量時Spark
會把數據保存到磁盤上。 不過在保存時每次會處理一個key
的數據,所以當單個 key 的鍵值對超過內存容量會存在內存溢出的異常。 這將會在之后發行的 Spark 版本中更加優雅地處理,這樣的工作還可以繼續完善。 盡管如此,仍應避免將數據保存到磁盤上,這會嚴重影響性能。

你可以想象一個非常大的數據集,在使用 reduceByKey 和 groupByKey 時他們的差別會被放大更多倍。
我們來看看兩個函數的實現:
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
/**
* Note: As currently implemented, groupByKey must be able to hold all the key-value pairs for any
* key in memory. If a key has too many values, it can result in an [[OutOfMemoryError]].
*/
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)
bufs.asInstanceOf[RDD[(K, Iterable[V])]]
}
注意mapSideCombine=false
,partitioner是HashPartitioner
,但是groupByKey對小數據量比較好,一個key對應的個數少于10個。
他們都調用了combineByKeyWithClassTag
,我們再來看看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)]
combineByKey函數主要接受了三個函數作為參數,分別為createCombiner、mergeValue、mergeCombiners。這三個函數足以說明它究竟做了什么。理解了這三個函數,就可以很好地理解combineByKey。
combineByKey是將RDD[(K,V)]combine為RDD[(K,C)],因此,首先需要提供一個函數,能夠完成從V到C的combine,稱之為combiner。如果V和C類型一致,則函數為V => V。倘若C是一個集合,例如Iterable[V],則createCombiner為V => Iterable[V]。
mergeValue則是將原RDD中Pair的Value合并為操作后的C類型數據。合并操作的實現決定了結果的運算方式。所以,mergeValue更像是聲明了一種合并方式,它是由整個combine運算的結果來導向的。函數的輸入為原RDD中Pair的V,輸出為結果RDD中Pair的C。
最后的mergeCombiners則會根據每個Key所對應的多個C,進行歸并。
例如:
var rdd1 = sc.makeRDD(Array(("A", 1), ("A", 2), ("B", 1), ("B", 2),("B",3),("B",4), ("C", 1)))
rdd1.combineByKey(
(v: Int) => v + "_",
(c: String, v: Int) => c + "@" + v,
(c1: String, c2: String) => c1 + "$" + c2
).collect.foreach(println)
result不確定歐,單機執行不會調用mergeCombiners:
(B,1_@2@3@4)
(A,1_@2)
(C,1_)
在集群情況下:
(B,2_@3@4$1_)
(A,1_@2)
(C,1_)
或者
(B,1_$2_@3@4)
(A,1_@2)
(C,1_)
mapSideCombine=false
時,再體驗一下運行結果。
有許多函數比goupByKey好:
- 當你combine元素時,可以使用
combineByKey
,但是輸入值類型和輸出可能不一樣 -
foldByKey
合并每一個 key 的所有值,在級聯函數和“零值”中使用。
//使用combineByKey計算wordcount
wordsRDD.map(word=>(word,1)).combineByKey(
(v: Int) => v,
(c: Int, v: Int) => c+v,
(c1: Int, c2: Int) => c1 + c2
).collect.foreach(println)
//使用foldByKey計算wordcount
println("=======foldByKey=========")
wordsRDD.map(word=>(word,1)).foldByKey(0)(_+_).foreach(println)
//使用aggregateByKey計算wordcount
println("=======aggregateByKey============")
wordsRDD.map(word=>(word,1)).aggregateByKey(0)((u:Int,v)=>u+v,_+_).foreach(println)
foldByKey
,aggregateByKey
都是由combineByKey實現,并且mapSideCombine=true
,因此可以使用這些函數替代goupByKey。