1.2spark源码阅读笔记 RDD PairRDDFunctions

上一篇介绍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 :25

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

1.2spark源码阅读笔记 RDD PairRDDFunctions_第1张图片

可以看到不会再次shuffle一次,也可以说明并不是某个方法就一定会触发shuffle,而是partitioner的变化会导致shuffle,所以在有些地方我们注意一点的话,也许是可以减少shuffle次数的 ,比如在mapparitionsRDD中,有是否保留partitioner的参数partitionpreserving,有兴趣的同学可以去参悟一下

你可能感兴趣的:(1.2spark源码阅读笔记 RDD PairRDDFunctions)