Spark RDD 源码分析

概述

概述:RDD是分布式数据集,代表了不可变、分区的元素集合,这些元素可以并行操作。
RDD有五个主要属性:
* -partition列表,和hadoop类似, 可切分的数据才能并行计算
* -计算每个split的function,RDD里面的compute函数
* -对于其他RDD的依赖列表,分宽、窄(依赖)两种,不是所有的RDD都有
* -(可选)对于key-value类型的RDD来讲,一个分区器,(比如:来说明这RDD是hash分区的),类似于hadoop的Partitioner接口,控制key分配到那个reduce。
* -(可选)计算每个split的preferred location(比如:一个HDFS文件的块location)。
对应于源码,有4个方法和一个属性:
protected def getPartitions: Array[Partition]
protected def getDependencies: Seq[Dependency[_]] = deps
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
def compute(split: Partition, context: TaskContext): Iterator[T]
@transient val partitioner: Option[Partitioner] = None

RDD转化

一个简单程序,wordcount

val data=sc.textFile(“data.txt”)
val flatRdd=data.flatMap(s=>s.split(“\\s+”))
val filterRdd=flatRdd.filter(_.length>=2)
val mapRdd=filterRdd.map(work=>(word,1))
val reduce=mapRdd.reduceByKey(_+_)

对应源码:

def textFile(path: String, minPartitions: Int = defaultMinPartitions): RDD[String] = {
//hdfs地址,InputFormat类型,Mapper的第一个类型,Mapper的第二个类型。
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}

sc.textFile调用hadoopFile方法,hadoopFile返回HadoopRdd,经过map后变为MappredRDD,经过flatMap是一个FlatMappedRDD,经过filter方法之后生成了一个FilteredRDD,经过map函数之后,变成一个MappedRDD,通过隐式转换(这个了解就行了),最后经过reduceByKey。

hadoopFile方法,里面我们看到它做了3个操作。
a、 把hadoop的配置文件保存到广播变量里。
b、设置路径的方法
c、new了一个HadoopRDD返回
HadoopRDD这个类的getPartitions、compute、getPreferredLocations。
先看getPartitions,它的核心代码如下:

val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
val array = new Array[Partition](inputSplits.size)
for (i <- 0 until inputSplits.size) {
array(i) = new HadoopPartition(id, i, inputSplits(i))
}

它调用的是inputFormat自带的getSplits方法来计算分片,然后把分片HadoopPartition包装到到array里面返回。
看compute方法,它的输入值是一个Partition,返回是一个Iterator[(K, V)]类型的数据,这里面我们只需要关注2点即可。
1、把Partition转成HadoopPartition,然后通过InputSplit创建一个RecordReader
2、 重写Iterator的geNext方法,通过创建的reader调用next方法读取下一个值。

//把partition转化成HadoopPartition
val split = theSplit.asInstanceOf[HadoopPartition]
logInfo("Input split: " + split.inputSplit)
var reader: RecordReader[K, V] = null
val jobConf = getJobConf()
val inputFormat = getInputFormat(jobConf)
HadoopRDD.addLocalConfiguration(new SimpleDateFormat("yyyyMMddHHmm").format(createTime),
context.stageId, theSplit.index, context.attemptId.toInt, jobConf)
//通过InputFormat创建一个RecordReader
reader = inputFormat.getRecordReader(split.inputSplit.value, jobConf, Reporter.NULL)
override def getNext() = {
try {
finished = !reader.next(key, value)
} catch {
case eof: EOFException =>
finished = true
}
(key, value)
}

从这里我们可以看得出来compute方法是通过分片来获得Iterator接口,以遍历分片的数据。
getPreferredLocations方法就更简单了,直接调用InputSplit的getLocations方法获得所在的位置。
有关依赖:
sc.textFile方法调用hadoopFile方法后(返回HadoopRDD),紧接着调用map方法,HadoopRDD中没有此方法,调用父类(RDD)方法,

def map[U: ClassTag](f: T => U): RDD[U] = new MappedRDD(this, sc.clean(f))

map会新建MappedRDD,有两个参数:this(hadoopRDD)和f。
注意:spark1.3版本中map直接生成MapPartitionRDD。
下面看MappedRDD:

class MappedRDD[U: ClassTag, T: ClassTag](prev: RDD[T], f: T => U)
extends RDD[U](prev) {
override def getPartitions:Array[Partition]= firstParent[T].partitions
override def compute(split: Partition, context: TaskContext) =
firstParent[T].iterator(split, context).map(f)
}

MappedRDD的主构造器中有两个参数prev和f,prev中传入hadoopRDD的实例,MappedRDD会调用父类的构造器RDD[U](prev),并且将hadoopRDD实例传递进去。RDD的这个构造器是辅助构造器:

def this(@transient oneParent: RDD[_]) =
this(oneParent.context , List(new OneToOneDependency(oneParent)))

会继续调用RDD的主构造器。此处的oneParent就是传入的hadoopRDD实例。再看RDD的主构造器:

abstract class RDD[T: ClassTag](
@transient private var sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging

可以看到deps是根据hadoopRDD实例创建的OneToOneDependency。至此,可以明确hadoopRDD成为MappedRDD的父依赖。
另外一点:在MappedRDD和FilterRDD中都用到firstParent[T],其定义:

//dependencies是一个Seq,head方法获取的是其第一个元素,dependencies.head返回的是一个dependency,dependency有一个rdd方法,返回和dependency的RDD。
protected[spark] def firstParent[U: ClassTag] = {
dependencies.head.rdd.asInstanceOf[RDD[U]]
}

到这儿基本明确了firstParent是什么,继续深入就比较复杂,涉及到checkpoint的东西,比较不好理解。
Dependencies是一个Dependency的序列,

private var dependencies_ : Seq[Dependency[_]] = null
//以Seq的形式,返回一个RDD的dependendies。
final def dependencies: Seq[Dependency[_]] = {
checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
if (dependencies_ == null) {
dependencies_ = getDependencies
}
dependencies_
}
}

在获取dependencies的过程中,用到了checkpointRDD,

private def checkpointRDD: Option[RDD[T]] = checkpointData.flatMap(_.checkpointRDD)

此时用到了checkpointData,是Option类型,其实例是scala.Some或者None的实例,Some[A]代表了一个A类型的值。

private[spark] var checkpointData: Option[RDDCheckpointData[T]] = None

这个类包含了所有和RDD checkpointing相关的信息。这个类的每一个实例都和一个RDD相关。
现在回到dependencies方法,如果存在checkpointRDD的话,则从checkpointRDD获取依赖信息,如果不存在,则调用getDependencies方法回去依赖。

protected def getDependencies: Seq[Dependency[_]] = deps

到这一步,所有的依赖关系都已经明确。
可以看出在MappedRDD中,
1、getPartitions直接沿用了父RDD的分片信息
2、compute函数是在父RDD遍历每一行数据时套一个匿名函数f进行处理,它的两个显著作用:
a、在没有依赖的条件下,根据分片的信息生成遍历数据的Iterable接口
b、在有前置依赖的条件下,在父RDD的Iterable接口上给遍历每个元素的时候再套上一个方法
还有最后一个转换reduceByKey(注意这是一个transformation操作,reduce是一个action操作),这个操作在RDD中找不到,此处用到了一个隐式的转换

implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null) = {
new PairRDDFunctions(rdd)
}

在SparkContext.scala中定义了很多隐式转化,当找不到相应方法是,可以去查下隐式转换。
reduceBykey的代码:

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = {
combineByKey[V]((v: V) => v, func, func, partitioner)
}

可以看到调用了combineByKey方法,下面是其主要代码:

val aggregator = new Aggregator[K, V, C](createCombiner, mergeValue, mergeCombiners)
//如果rdd有partitioner,会创建新的
if (self.partitioner == Some(partitioner)) {
self.mapPartitionsWithContext((context, iter) => {
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
}, preservesPartitioning = true)
//如果没有partitioner,创建新的shuffledRDD
} else {
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}

如果有parittioner的话(这部分RDD转化比较繁琐,所以只写了大致过程):aggregator的combineValuesBykey方法将[k,v]类型的数据聚集成为[k,c],返回值为一个迭代器。InterruptibleIterator中的元素类型和其构造器第二个参数类型一致,即[k,c]类型,也就是说将[k,v]类型的数据聚集成为了[k,c]类型,(例如[1,1][1,2][2,2][3,3],如果聚集函数是(+”####”+), 那么结果是[1,1####2][2,2][3,3])。然后调用RDD的mapPartitionsWithContext方法,此方法会创建新的MapPartitionsRDD,MapParitionsRDD的代码:

private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
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
/**
* 注意这个地方,compute中的f是在iterator之外的,这样才能对iterator的所有数据进行一个合并。
* */
override def compute(split: Partition, context: TaskContext) =
f(context, split.index, firstParent[T].iterator(split, context))
}

如果没有,则会创建新的ShuffledRDD.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值