从装饰者的角度来观察RDD
大家好,我是一拳就能打还是爆A柱的硬核男人
好久没有更新博客了,不是我没有干活,而是我暂时迷茫找不到目标了。这段时间真的恶心,迷茫到爆,每天看点博客,有时看点书,每天好像开开心心的,可是我没动力了。突然就没动力了,不知道为什么。但是还好,经过调整我还是决定尽量每天都写点博客,针对一个问题深挖下去,记录下来分享给大家。也不知道我这个辣鸡博主有没有人看。最近也是有点其他的收获的,比如我发现我之前对RDD的理解还是有偏差,所以今天我决定去翻RDD的源码希望能给大家带来点新东西。
1、设计模式 - 装饰者
在了解RDD之前,我们需要去看装饰者的定义,因为RDD用到了这个设计模式,根据百度百科的解释:
装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
所以说装饰者模式可以通过继承的方式扩展对象的功能,就像在外面包了一层一样。
在博客《装饰者模式》中,记录了装饰者模式的示例,我摘取一部分:
在这个继承树中,右方的AttachedPropertiesDecorator作为抽象装饰者,但是其实现类可以在这个基础上拓展出addCar、addHouse、addDeposit、addQuality方法。通过这一系列的装饰者可以对Man拓展出许多功能,而Man类作为成员对象出现在装饰者家族中,完全是被动的、不知情的,也就可以被动的赋予许多功能。大家感兴趣可以去看一看这篇博客。
在《尚硅谷2021迎新版大数据Spark从入门到精通P25》中关于RDD的描述也通过装饰者的角度解读了RDD,其中使用的例子是Java的IO流操作,不同的对象负责一部分的功能,这些功能的叠加最终实现了字符的IO。
按字节输入的读取案例:
FileInputStream in = new FileInputstream("1.png");
BufferedInputStream buffIn = new BufferedIntputStream(in);
buffIn.read
FileInputStream读取字节,读取完字节后将数据放入BufferedIntputStream的Buffer中。这样做可以减少交互次数,从而提高读取效率。
按字符输入的读取案例:
Reader in = new BufferedReader(
new InputStreamReader(new FileInputStream("path"), "UTF-8")
);
如下图:
FileInputStream负责读取字节,InputStreamReader将字节在缓冲区中转义,最后送到BufferedReader缓冲区。
2、 从wordCount入手
目前了解了装饰者模式,对它有个大概的印象,接下来我们从wordCount案例来看RDD:
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object wc {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("wc")
val sc = new SparkContext(conf)
val file: RDD[String] = sc.textFile("1.txt")
val flat: RDD[String] = file.flatMap(_.split(" "))
val value: RDD[(String, Int)] = flat.map(word => (word, 1))
val value1: RDD[(String, Int)] = value.reduceByKey(_ + _)
value1.collect().foreach(println)
}
}
第一行
根据第一行去读取文件得到了一个RDD:
val file: RDD[String] = sc.textFile("1.txt")
第二行
所以接下来看下一行flatMap:
val flat: RDD[String] = file.flatMap(_.split(" "))
点击进入flatMap方法:
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}
在方法内部可以知道,f是具体的计算方法,方法第一行是做环境清理,第二行直接生成MapPartitionsRDD,该构造方法传入的对象有当前的RDD(this),也就是说flatMap生成的MapPartitionsRDD将源RDD作为了它的成员变量(这里不讨论f计算方法如何操作,重点观察RDD的封装)。
flatMap最后返回的还是RDD类型,显然MapPartitionsRDD也是RDD抽象类的子实现类。
第三行
val value: RDD[(String, Int)] = flat.map(word => (word, 1))
map方法传入计算规则,最后生成新的RDD返回,点击看map:
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
同样的,做了clean,然后生成了新的MapPartitionsRDD,也就是说现在的关系是:[MapPartitionsRDD[MapPartitionsRDD[RDD]]],MapPartitionsRDD里包了MapPartitionsRDD里包了RDD。
第四行
val value1: RDD[(String, Int)] = value.reduceByKey(_ + _)
点击进入reduceByKey:
def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
reduceByKey(defaultPartitioner(self), func)
}
进入下一层:
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
继续下一层:
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 {
......
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)
}
}
省略其他无关的计算,最后的if-else可以看到要么调用mapPartitions,要么new一个ShuffledRDD,显然mapPartitions也是生成一个MapPartitionsRDD。所以最后reduceByKey又是在原有的基础上包了一层外衣。
现在回头看Java-IO的图是不是很像,我们重新思考一下:
- Java-IO中FileInputStream负责读取字节,InputStreamReader负责将多字节转成字符,BufferedReader负责将多个字符缓存以减少连接次数提高效率。
- Spark-wordCount中textFile产生的RDD负责从数据源读取数据,flatMap负责按规则1计算数据,map负责按规则2计算数据,reduceByKey负责按规则3计算数据。
注意:上面包了那么多层还是一个RDD,注意看上面的继承树。
各位想想,RDD的操作是不是都是在规定一些规则,然后一层层的包下去。在最底层的操作做好了之后上一层继续,所以应该是可以将这些操作定义成lazy操作,因为只要数据到位后面对数据的操作都可以顺理成章的进行下去。实时也正是如此的,Spark将RDD的操作分为两大类,一类transformation,一类action。只有当action执行的时候才知道数据需要输出,这也就意味着一个计算阶段的结束,所以前面这一段transformation操作可以连续执行了。
transformation操作属于懒加载操作,只有当action触发的时候才会将前面的一系列操作执行下来。这就好比我们定义一个函数:
def compute(x : Int) : Int = {
y = x+1
y2 = y-2
y3 = y2*3
y4 = y3*y3
return y4
}
这一系列的对x的操作,只要主调函数传入x值(action),那么之前定义好的操作(transformation)都会像流水线一样执行下来。
关于如何划分阶段(stage)还需要单独一篇博客才能讲清楚,所以这里还是先不提了。
总结
RDD遵循装饰者模式,而且经过wordcount案例可以知道RDD中的一系列算子就是对数据操作的定义,这只是定义而不是具体执行。只有待action触发后,之前的transformation才会执行,这就好像上面的compute函数定义,action未触发则不会执行。
这篇博客写的太差了,我自己都看不下去,好久没动笔去写东西了,所以质量很差,我接下来每天会坚持写一篇,每天针对一个问题去尽可能深的挖下去,把我的见解都记录下来。