上一篇介绍RDD的文章里,大概介绍了一些抽象类RDD,其中包括了一些RDD共通的方法,但是仔细查看发现,还有很多我们常用方法并没有在其中,比如reduceByKey,combineByKey等等,甚至找了几个RDD的实现类,发现都没有找到对应的方法。直到发现这个PairRDDFunctions,原来这几个方法都在这个PairRDDFunctions类中,那么这个类和RDD是如何关联,如何使用的呢?
先来一坨PairRDDFunctions的构造函数
class PairRDDFunctions[K, V](self: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null)
extends Logging with Serializable
感到奇怪的是,这个PairRDDFunctions并没有继承RDD抽象类,也就是说并没有pairRDD这样一个子类。具体看一下构造函数,这里可能有几个地方需要向和我一样的scala小白解释一下
1.首先泛型,这里[K,V]表示泛型,代表RDD中pair的key的类型,和value的类型。这里大概解释一下我所理解的泛型,其实泛型就是我们再写代码时告诉编译器,我们可能要用到什么样的类。以这里为例,就是告诉下编译器,我们要用到K这个类,但是我不知道现在不知道这个类是什么,只知道就是RDD里key的类型。利用这个我来约束我这个类中方法和变量,比如类中reduceByKey方法
def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
reduceByKey(defaultPartitioner(self), func)
}
这个根据K,V的泛型约束,这个入参的func必须是(V,V)=>V的类型,也就是操作两个Value而不是Key,reduceByKey返回的结果也必须是key为K类型,value为V类型的rdd。
2.implicit关键字 隐式
作为一个从java过来的scala菜鸟,一直以为implicit就是一个强转的意思,看到这里才发现自己实在太低估这个implicit了。
implicit关键字大概有三种用法,分别为隐式参数,隐式转换,隐式类
隐式参数:
首先介绍下构造函数中第二行,(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null)。这里是implicit关键字的一个用法,隐式参数。在函数中使用到kt这个对象时,scala会找到对应的ClassTag对象,也就是kt注入到函数中
举个例子
scala> implicit val x:Int=100
x: Int = 100
scala> def add(g:Int)(implicit t:Int)=t+g
add: (g: Int)(implicit t: Int)Int
scala> add(1)
res2: Int = 101
这里在add函数中,定义了一个隐式参数t,然后在调用t+g时,scala会找到对应的t,具体找寻路径主要有两个
1.首先,会去作用域中找寻implicit定义的参数,或者,implicit定义的返回值为对应类型的方法
2.如果没有的话,则会去伴生对象中找寻implicit定义的参数和方法
例子中,就是走了上述中的第一个,直接找到了implicit定义的合适的参数,implicit val x:Int=100,然后t就指向了x,再回到spark这里来,当有多个隐式参数时,只需要在第一个前面加上implicit即可,implicit kt: ClassTag[K]是一种常用的反射方法,在ClassTag中定义了相关的隐式参数,会返回一个K类型的ClassTag对象。
隐式转换
虽然看懂了构造函数,但是还是不明白RDD是如何能够使用PairRDDFunctions中的方法,或者说RDD是如何转换为PairRDDFunctions类的,其实这里是利用implicit关键字的第二种用法,隐式转换,下面看一下RDD伴生对象中的代码
object RDD {
private[spark] val CHECKPOINT_ALL_MARKED_ANCESTORS =
"spark.checkpoint.checkpointAllMarkedAncestors"
// The following implicit functions were in SparkContext before 1.3 and users had to
// `import SparkContext._` to enable them. Now we move them here to make the compiler find
// them automatically. However, we still keep the old functions in SparkContext for backward
// compatibility and forward to the following functions directly.
implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
new PairRDDFunctions(rdd)
}
可以看到这里还是利用implicit关键字,利用PairRDDFunctions类作为RDD的类增强。当我们构造了一个pairrdd后,调用类似reduceByKey等方法时,会将RDD转换为PairRDDFunctions,也就是隐式转换。当然这个转换也不是随时随地可以实现的,其中RDD的类型必须要满足rdd: RDD[(K, V)这个入参的形式,也就是pair形式。隐式转换的触发大概也有两种,
1.入参类型不匹配
2.没有对应的成员变量或者成员函数
再举个小例子
scala> implicit def t[A](x:List[A]):Ordered[List[A]]=new Ordered[List[A]]{def compare(that:List[A]):Int=1 }
warning: there was one feature warning; re-run with -feature for details
t: [A](x: List[A])Ordered[List[A]]
scala> List(1, 2, 3) > List(4, 5)
res2: Boolean = true
可以看到按道理两个List是无法直接比较的,这里触发了前面说的第二条,List类中没有对应的比较方法,所有会自动找寻作用域中,implicit修饰的入参为List,返回值的type中有,对应的比较方法成员的,也就是找到了t这个方法,实现List->Ordered类的隐式转换。
那么回到spark中,当一个pairrdd调用reduceByKey也是一样的原理,会调用rddToPairRDDFunctions方法,将RDD转换为我们需要的rddToPairRDDFunctions类
注:再spark1.3之前,想要实现这样的隐式转换,必须要提前import SparkContext._
--------------------------------------------------------------------------------------------------------------------------------------------------------------
接下来我们再具体介绍下rddToPairRDDFunctions类中的方法,主要有combinebykey,reducebykey,aggregatebykey等等bykey,但其实他们原理都是调用下面这个方法,就是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))
if (self.partitioner == Some(partitioner)) {
self.mapPartitions(iter => {
val context = TaskContext.get()
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
}, preservesPartitioning = true)
} else {
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}
}
大概来看一下这个方法的介绍,就是用一套组合拳来实现对于key-value的rdd[K,V],转换为另外一个key-value的rdd[K,C],这里可以注意到value的类型是可以变化的,而key的类型是不能变化的。同时这个方法也是最常用同时会出现shuffle的方法。
接下来看一下几个入参
createCombiner: V => C:某个partition中遇到某一个key的第一个key-value对时的处理方法
mergeValue: (C, V) => C:某个partition中遇到某一个key的第二个到最后一个key-value对时的处理方法
mergeCombiners: (C, C) => C:不同partition中计算结果,merge(融合)时使用的方法
这三个方法也就是一套组合拳的基本构成,实现了我们对于key的各种折腾,那么简单的以这个一个
RDD[Int,Int]=[(1,1),(1,2),(2,2),(2,2)],两个partition,分别有数据[(1,1)] [(1,2),(2,2)(2,2)]
然后reduceByKey((x,y)=>x+y) 求相同的key的所有value合值(先简单举个例子,后面再详细看一个),那么对于这个组合拳就是
createCombiner=x=>x:遇到第一个不做任何变化,两个partition,[(1,1)] [(1,2),(2,2)(2,2)]
mergeValue=(x,y)=>x+y 遇到下一个value时,就把它和当前的值加到一起(x可以理解为当前sum的值)两个partition,[(1,1)] [(1,2),(2,4)]
mergeCombiners=(x,y)=>x+y 将所有的partition的结果再加到一起,最后得到的结果RDD[Int,Int]=[(1,3),(2,2)]
可以看到这一套组合拳有一个专门的类,就是Aggregator[K, V, C]。
再看一下其他三个入参
partitioner: Partitioner 专门来记录partition的数量,和partition的分配方法
mapSideCombine: Boolean = true,可以理解为是否在map端进行聚合
serializer: Serializer = null:进行shuffle时所需的序列化方法
接下来的代码就好理解了
当key的类型是array时,partitioner不能用hash分配的方法,不能再map端聚合,如果partitioner相同(不一定是同一个对象,spark自己定义了partitioner的比较方法)则不会发生shuffle,这里可以尝试连续连词reduceByKey,是只出现一次shuffle。如果partitioner不同,则会出现shuffle。
为了更好的理解这个方法,我尝试实现这个一个需求,((1,1),(1,2),(1,5),(2,1),(2,4),(2,3),(3,1))将这样一个pairrdd的value都加一,然后相同key的拼接起来,同时看一下原本数据都属于哪个partition
scala> var m=sc.makeRDD(List((1,1),(1,2),(1,5),(2,1),(2,4),(2,3),(3,1)))
m: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[5] at makeRDD at
scala>m.combineByKeyWithClassTag[String](
(b:Int)=>(b+1).toString,
(k:String,v:Int)=>k+(v+1),
(v1:String,v2:String)=>v1+" "+v2)
.collect
res14: Array[(Int, String)] = Array((1,2 36), (2,25 4), (3,2))
可以看到这里createCombiner:(b:Int)=>(b+1).toString,讲遇到的第一个value加一并变成string型
mergeValue:(k:String,v:Int)=>k+(v+1),将后面遇到int都拼接到String的后面
mergeCombiners:(v1:String,v2:String)=>v1+“ ”+v2) 不同partition的结果用空格隔开
可以看到最后的结果如上,同时也发生了shuffle,加入我们再次reducebykey一次
m.combineByKeyWithClassTag[String]((b:Int)=>(b+1).toString,(k:String,v:Int)=>k+(v+1),(v1:String,v2:String)=>v1+" "+v2).reduceByKey((x,y)=>x+y+1).collect
可以看到不会再次shuffle一次,也可以说明并不是某个方法就一定会触发shuffle,而是partitioner的变化会导致shuffle,所以在有些地方我们注意一点的话,也许是可以减少shuffle次数的 ,比如在mapparitionsRDD中,有是否保留partitioner的参数partitionpreserving,有兴趣的同学可以去参悟一下