刚刚开始学习scala,对于spark又有深入了解的需求,索性开始阅读spark的源码,之前照着spark-streaming-kafka的代码抄了一份spark连接cannal的代码,现在希望开始从spark最基础的spark-core模块开始深入了解一下
1 RDD
RDD是spark中最基本的逻辑数据单元,源码在RDD类记录了所有RDD共同的转换操作和行动操作,比如常用的MAP,Mappartitions,collect等等,首先看一下RDD的构造函数
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging
可以看到RDD是一个抽象类,对应不同的情况下会实例化出不同的RDD,其中参数 sc 表示运行的sparkcontext,deps 为这个RDD与父RDD所依赖的依赖关系,根据这个dependency类可以找到这个RDD对应的父RDD,可以理解为spark dag执行顺序的连接脉络。
其中Dependency类有两个子类,分别对应宽依赖和窄依赖,具体宽依赖和窄依赖的原理就不细说了,可以暂时理解为依赖有可能只依赖一个父RDD,也有可能依赖多个父RDD(这不代表宽依赖多个RDD,窄依赖只依赖一个RDD哦),所以deps为dependency的一个列表
接下来是RDD中几个重要抽象方法
/** * 记录了rdd接下来要进行的操作 */ @DeveloperApi def compute(split: Partition, context: TaskContext): Iterator[T] /** * 如何分partition */ protected def getPartitions: Array[Partition] /** * partition具体分发到那个work的策略实现 */ protected def getPreferredLocations(split: Partition): Seq[String] = Nil
在自己实现rdd时最基本要实现的几个方法就是上述三个,以实现类MapPartitionsRDD为例,看看具体如何实现上述方法
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag]( var prev: RDD[T], f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator) preservesPartitioning: Boolean = false) extends RDD[U](prev) { override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None override def getPartitions: Array[Partition] = firstParent[T].partitions override def compute(split: Partition, context: TaskContext): Iterator[U] = f(context, split.index, firstParent[T].iterator(split, context))
构造函数中prev为父RDD,f为接下来的操作,如map,mappartition等。在构造函数中,调用了父类RDD的单参数构造方法
def this(@transient oneParent: RDD[_]) = this(oneParent.context, List(new OneToOneDependency(oneParent)))
其中OneToOneDependency,为窄依赖NarrowDependency的一个子类,这个构造函数表明,构成一个MapPartitionsRDD的操作一定是一个窄依赖操作,而且只依赖于一个父RDD,不会发生shuffle,所以context就也可以直接利用父RDD的context,因为同在一个stage中。
接下来
override def getPartitions: Array[Partition] = firstParent[T].partitions
由于是一对一的窄依赖,所以与父RDD的partition也不会发生变化,直接使用了firstParent。这里firstParent是之前说到RDD构造中可能会有多个依赖,所以是个依赖的list,firstParent是去依赖中的第一个依赖中的RDD(感觉这里我理解也不甚到位,可以暂时这么理解吧)
再然后
override def compute(split: Partition, context: TaskContext): Iterator[U] = f(context, split.index, firstParent[T].iterator(split, context))
conpute函数如前面所说,是记录了这个mapRDD接下来要进行的操作,这样说可能有些迷茫,我们以mapPartitions方法举个例子,
val x=sc.parallelize(1 to 9) val partitionRDD=x.mapPartitions(it=>it.map(_+3)) partitionRDD.collect()
x为ParallelCollectionRDD,也是RDD的一个实现类,没有依赖的父RDD,暂时先按一个普通RDD处理。
然后,再x进行mapPartitions后,生成了一个mapPartitionsRDD,此时mapPartitionsRDD的依赖为x,compute中记录了it=>it.map(_+3)这个方法,就是将迭代器中每个元素加三这个方法。
最后partitionRDD执行RDD中公共的方法
def collect(): Array[T] = withScope { val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray) Array.concat(results: _*) }
可以看到collect会提交job,并且将每个partition中compute的结果,变成array,最后拼接到一起,至此一个简单的job就完成了,具体runjob中式怎样执行的,需要理解spark中另外一个重要的类dagscheduler。
之后会再去理解一下宽依赖和dagscheduler,希望自己能够坚持下去吧,第一次写,乱糟糟的,各位看官见谅