RDD是spark计算的核心,是分布式数据元素的集合,具有不可变、可分区、可被并行操作的特性,基础的RDD类包含了常用的操作,如果需要特殊操作可以继承RDD基类进行自己的扩展,基础预算包括map、filter、reduce等。
RDD包含5个主要特性:partition、针对split的算子、自身依赖哪些RDD、分区类型(默认hash)、split计算是的分区位置(例如计算HDFS block的时候,让计算分配在block所在机器会减少网络数据的传输)
如果需要自定义一个RDD,通常需要实现如下方法:
//对一个partition做何种运算
def compute(split: Partition, context: TaskContext): Iterator[T]
//获得该RDD的所有分区
protected def getPartitions: Array[Partition]
//获得该RDD有哪些依赖
protected def getDependencies: Seq[Dependency[_]] = deps
//获得partition的最佳位置
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
//使用何种分区
@transient val partitioner: Option[Partitioner] = None
在一个作业提交给dagscheduler前,要先构建RDD的DAG图,构建过程需要建立依赖关系,在spark中有宽依赖和窄依赖之分,下面我们通过一个具体的计算链条来分析这个过程,代码如下:
val count = sc.parallelize(1 to 5, 2).map((_,1)).reduceByKey(_+_).collect().foreach(println)
这段逻辑非常简单,构建1到5的数字集合,然后做一个wordcount操作,最后打印结果。
执行过程从左到右,首先执行parallelize操作:
def parallelize[T: ClassTag](seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}
返回的是一个ParalleCollectionRDD的RDD,这里只是构建一个RDD的对象,还并未运行任何计算,继续往下看会发现在action触发前,也就是job submit之前是不进行任何计算的,直到job提交后才会进行,这就是RDD的延迟计算。后面进行map操作最终返回MapPartitionsRDD
def map[U: ClassTag](f: T => U): RDD[U] = {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
注意上面代码的this,是上一个RDD,也就是ParallelCollectionRDD进入MapPartitionsRDD的构造函数可以看出,这个RDD其实是当前RDD的parent RDD,代码如下:
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
override def compute(split: Partition, context: TaskContext) =
f(context, split.index, firstParent[T].iterator(split, context))
}
这样RDD之间的依赖关系就构建好了,进行reducebykey操作前会进行隐式转换,将RDD转化为PairRDDFunctions,然后进行reduceByKey操作。
collect是一个action算子,上述map、reduce操作属于transform操作,只负责对RDD打转化标记,并不执行真正的计算。而action操作会导致作业提交至集群,由调度器做好切分stage后进行调度。
def collect(): Array[T] = {
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
runJob会触发作业的提交,客户端通过向AKKA服务器发送提交作业的消息,来提交作业,代码如下:
def submitJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
allowLocal: Boolean,
resultHandler: (Int, U) => Unit,
properties: Properties): JobWaiter[U] = {
// Check to make sure we are not launching a task on a partition that does not exist.
val maxPartitions = rdd.partitions.length
partitions.find(p => p >= maxPartitions || p < 0).foreach { p =>
throw new IllegalArgumentException(
"Attempting to access a non-existent partition: " + p + ". " +
"Total number of partitions: " + maxPartitions)
}
val jobId = nextJobId.getAndIncrement()
if (partitions.size == 0) {
return new JobWaiter[U](this, jobId, 0, resultHandler)
}
assert(partitions.size > 0)
val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, allowLocal, callSite, waiter, properties))
waiter
}
作业提交的消息中包含作业ID、最终的RDD,partitions等主要信息,注意这里的RDD是最终的RDD,因为通过这个RDD可以反推出所有依赖的RDD,所以提交时只要这一个就够了,后面我们会分析作业提交服务端是如何处理这个RDD的。