spark中有一些xxxByKey的算子。我们来看看。
groupByKey
解释
假设我们要对一些字符串列表进行分组:
object GroupByKeyOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd: RDD[String] = context.makeRDD(List(
"spark", "scala", "hive", "flink",
"kafka", "kafka", "hbase", "flume",
"sqoop", "hadoop", "kafka", "spark",
"flink", "kafka", "kafka", "hbase"
),4)
val mapRDD: RDD[(String, Int)] = rdd.map((_,1))
val groupByRDD: RDD[(String, Iterable[Int])] = mapRDD.groupByKey()
groupByRDD.saveAsTextFile("groupby_out")
TimeUnit.MINUTES.sleep(50)
context.stop()
}
}
那会出现什么情况呢?为了查看DAG图,我让程序睡眠了。
groupByKey
存在了shuffle操作。后续我们会详细解释shuffle。
当下,我们只需知道在stage1出现了一个ShuffledRDD
:
进入groupByKey
的源码:
def groupByKey(): RDD[(K, Iterable[V])] = self.withScope {
groupByKey(defaultPartitioner(self))
}
首先是要拿到默认的分区器。
假设我们没有自己的分区器,所以就会使用HashPartitioner
。
再假设也没有设定
spark.default.parallelism
的值,
那么就会取几个rdd中分区数最大的那个值,然后根据这个值来构造HashPartitioner
。为什么会有几个rdd呢?rdd是可以join的。
所谓的分区,就是key的hashcode
值模上分区数,再保证非负即可。
拿到了分区器,我们进入groupByKey
:
他这里有3个函数:
val createCombiner = (v: V) => CompactBuffer(v)
首先是把组内的第一个数放进CompactBuffer
。
val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v
然后将组内其他的数放入CompactBuffer
。
val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
最后是多个CompactBuffer
的大合并。
进入combineByKeyWithClassTag
,我们看到:
他把3个函数丢给了Aggregator
,然后把Aggregator
丢给了ShuffledRDD
。
不妨看一下程序运行的结果:
为什么是4个分区,因为我们只有一个rdd,这个rdd就是4个分区的(指定的)。
为什么hive
和flink
都在0号分区,这是HashPartitioner
的getPartition
计算的结果。
为什么所有的1被包裹在CompactBuffer
中,因为groupByKey
就是用CompactBuffer
来保存分组结果的。
至于这个CompactBuffer
:
由于包访问权限,我们也用不了。但他说了,这个东西和ArrayBuffer
很像。所以我们用ArrayBuffer
来替代CompactBuffer
。
实现
既然我们已经看到了所有事实,我们就来模仿一个groupByKey
吧。
因为最后需要的是ShuffledRDD
,它要什么我们给他什么。
主构造函数要一个之前的rdd和一个分区器,这个好说。
那么那3个泛型是啥?
看看人家是怎么写的。
那三个泛型和Aggregator
的三个泛型一样。
所以先new一个Aggregator
。所以要先准备三个函数。
那三个函数甚至可以直接从groupByKey
偷来:
根据三个函数的类型,我们得知V
在我们实现的代码里就是Int
,C
就是ArrayBuffer
(用ArrayBuffer
替代CompactBuffer
)。
所以K
是什么?
ShuffledRDD
里面其实说清楚了,K
是key class,我们这里就是String
咯。
所以最终的代码是:
object GroupByKeyOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd: RDD[String] = context.makeRDD(List(
"spark", "scala", "hive", "flink",
"kafka", "kafka", "hbase", "flume",
"sqoop", "hadoop", "kafka", "spark",
"flink", "kafka", "kafka", "hbase"
),4)
val mapRDD: RDD[(String, Int)] = rdd.map((_,1))
val createCombiner = (v:Int) => ArrayBuffer[Int](v)
val mergeValue = (buf: ArrayBuffer[Int], v: Int) => buf += v
val mergeCombiners = (c1: ArrayBuffer[Int], c2: ArrayBuffer[Int]) => c1 ++= c2
val aggregator: Aggregator[String, Int, ArrayBuffer[Int]] = new Aggregator[String, Int, ArrayBuffer[Int]](createCombiner, mergeValue, mergeCombiners)
val shuffledRDD: ShuffledRDD[String, Int, ArrayBuffer[Int]] = new ShuffledRDD[String, Int, ArrayBuffer[Int]](mapRDD, new HashPartitioner(rdd.partitions.length))
shuffledRDD.setMapSideCombine(false)
shuffledRDD.setAggregator(aggregator)
shuffledRDD.saveAsTextFile("shuffledRDD_out")
context.stop()
}
}
至于为什么要
shuffledRDD.setMapSideCombine(false)
人家有说,就是map端不要combine,你是分组,你就算用了combine也不会减少数据量。然后会让map端的所有数据插进大大的hash table中,使老年代压力很大。
关于groupByKey
最后要注意的是这其实是个隐式转换。
groupByKey
是由RDD
调用的,但是RDD
里面没有这个方法,这个方法在PairRDDFunctions
中。
所以,RDD
一定是被PairRDDFunctions
包裹了:
groupBy
说到groupbyKey
,那就要顺便说一下groupBy
。
看一个简单的例子:
object GroupByOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd = context.makeRDD(List("Hello", "Spark", "Hi", "Scala"))
val f = (str: String) => str.charAt(0)
val groubyRdd = rdd.groupBy(f)
groubyRdd.collect().foreach(println)
context.stop()
}
}
结果:
(S,CompactBuffer(Spark, Scala))
(H,CompactBuffer(Hello, Hi))
也就是说,groupBy
要By什么更加灵活,而groupByKey
只能是按对偶元组的第一个元素分组。
点进源码看看:
他其实还是变成了一个对偶元组然后调用groupByKey
,所以我们的测试代码本质上是:
object GroupByOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd = context.makeRDD(List("Hello", "Spark", "Hi", "Scala"))
val f = (str: String) => str.charAt(0)
// val groubyRdd = rdd.groupBy(f)
val groupbyRdd: RDD[(Char, Iterable[String])] = rdd.map(t => (f(t), t)).groupByKey()
groupbyRdd.collect().foreach(println)
context.stop()
}
}
reduceByKey
我们刚刚看了PairRDDFunctions
这个类了:
他这里有大量的xxByKey操作。我们再看看reduceByKey
。
来个例子:
object ReduceByKeyOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val dataRDD = context.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("a", 4), ("c", 5)))
val func = (v: Int, w: Int) => v + w
val reduceRDD: RDD[(String, Int)] = dataRDD.reduceByKey(func)
reduceRDD.collect().foreach(println)
context.stop()
}
}
结果:
(a,5)
(b,2)
(c,8)
所以reduceByKey
只是比groupByKey
多了聚合的功能。
底层调用的还是combineByKeyWithClassTag
。
参数中的第一个函数就是将一个值取出来占个位(什么也不做)。
我们写的func在局部聚合和全局聚合都使用了。
最底层当然还是一个ShuffledRDD
。
distinct
distinct
就是去重。
比如:
object DistinctOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd = context.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4, 5))
rdd.distinct().collect().foreach(println)
context.stop()
}
}
为什么要在reduceByKey
下面讲distinct
呢?
我们点进distinct
的源码:
如果修改我们的程序,大概是这样的:
object DistinctOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd = context.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4, 5))
// rdd.distinct().collect().foreach(println)
rdd.map(x => (x, null)).reduceByKey((x, _) => x).map(_._1).foreach(println)
context.stop()
}
}
他这里去重用的就是reduceByKey
。
aggregateByKey
aggregateByKey
的使用更加地有弹性。
首先他有初始值,这和foldByKey
一样,然后还有区内函数和分区间的函数。
我们需要知道,初始值被用在了什么地方:
可以看到初始值只被作用在区内函数中。
举个例子:
object AggregateByKeyOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd: RDD[(String, Int)] = context.makeRDD(List(("a", 1), ("a", 2), ("c", 3), ("b", 4), ("c", 5), ("c", 6)),2)
val seqOp = (x: Int, y: Int) => math.max(x, y)
val combOp = (x: Int, y: Int) => x + y
val aggRDD: RDD[(String, Int)] = rdd.aggregateByKey(10)(seqOp, combOp)
aggRDD.saveAsTextFile("agg-out")
}
}
先看看运行的结果吧:
那aggregate
是如何做的呢?
我们造的rdd有两个分区,所以第一个分区为:
("a", 1), ("a", 2), ("c", 3)
第二个分区为:
("b", 4), ("c", 5), ("c", 6)
分区内是比较相同hashcode值的key的value的最大值,并且初始值也会参与分区内的运算,所以第一个分区中
("a", 1), ("a", 2), ("a", 10)
比较的结果就是:
("a", 10)
("c", 3), ("c", 10)
比较的结果就是
("c", 10)
至于第二个分区,最后会剩下:
("b", 10), ("c", 10)
全局聚合要求相同的key的value值相加。
所以有
("a", 10), ("c", 10+10), ("b", 10)
combineByKey
combineByKey
有点不同的就是传入的第一个函数可以变换数据结构。
我们来个需求:
求相同的key的值的平均值。
object CombineByKeyOperator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val context = new SparkContext(conf)
val rdd = context.makeRDD(List(
("a", 1), ("a", 2), ("b", 3),
("b", 4), ("b", 5), ("a", 6)
), 2)
val createCombiner = (x: Int) => (x, 1)
val mergeValue = (t: (Int, Int), v: Int) => (t._1 + v, t._2 + 1)
val mergeCombiners = (t1: (Int, Int), t2: (Int, Int)) => (t1._1 + t2._1, t1._2 + t2._2)
//获取相同key的平均值
val aggRdd: RDD[(String, (Int, Int))] = rdd.combineByKey(createCombiner, mergeValue, mergeCombiners)
val result: RDD[(String, Int)] = aggRdd.mapValues {
case (sum, count) => {
sum / count
}
}
result.collect().foreach(println)
context.stop()
}
}
我需要陈述传入的三个函数是如何工作的。
因为我们造的数据分了两个分区,所以
("a", 1), ("a", 2), ("b", 3)
为一个分区。
("b", 4), ("b", 5), ("a", 6)
为另一个分区。
首先看
("a", 1)
a这个key出现过吗?显然没有。所以应用createCombiner
。
于是("a", 1)
变成了a==>(1,1)
。这个表示的意思就是a
这个key所对应的值是(1,1)
。
然后看第二个数("a", 2)
。
还是问a
出现过吗?啊,a
出现过。所以应用第二个函数mergeValue
。那么如果做呢?
((1,1),2) => (1+2, 1+1)
得到的(3,2)
中3是数值的相加,2是a
的个数的相加。
再看第二个分区中key为a
的情况,因为只有一个a
,所以最后的结果就是:
a==>(6,1)
区内的计算结束了。
分区间的聚合需要mergeCombiners
函数。也就是(3,2)
要和(6,1)
应用该函数(我们一直在谈论key为a
的情况)。
所以最后就是:
((3,2),(6,1))=>(3+6,2+1)
这样我们就把key为a
的所有元组的值的和与出现的次数都统计出来了,最后相除即为平均数。
如此一来combineByKey
的工作原理就清晰了。