Spark RDD剖析
RDD简介
Spark计算中一个重要的概念就是可以跨越多个节点的可伸缩分布式数据集 RDD(resilient distributed
dataset) Spark的内存计算的核心就是RDD的并行计算。RDD可以理解是一个弹性的,分布式、不可变的、带有分区的数据集合,所谓的Spark的批处理,实际上就是正对RDD的集合操作,RDD有以下特点:
- 任意一个RDD都包含分区数(决定程序某个阶段计算并行度)
- RDD所谓的分布式计算是在分区内部计算的
- 因为RDD是只读的,RDD之间的变换存着依赖关系(宽依赖、窄依赖)
- 针对于k-v类型的RDD,一般可以指定分区策略(一般系统提供)
- 针对于存储在HDFS上的文件,系统可以计算最优位置,计算每个切片。(了解)
如下案例:
通过上述的代码中不难发现,Spark的整个任务的计算无外乎围绕RDD的三种类型操作RDD创建、RDD转换、RDD Action.通常习惯性的将flatMap/map/reduceByKey称为RDD的转换算子,collect触发任务执行,因此被人们称为动作算子。在Spark中所有的Transform算子都是lazy执行的,只有在Action算子的时候,Spark才会真正的运行任务,也就是说只有遇到Action算子的时候,SparkContext才会对任务做DAG状态拆分,系统才会计算每个状态下任务的TaskSet,继而TaskSchedule才会将任务提交给Executors执行。现将以上字符统计计算流程描述如下:
textFile("路径",分区数)
-> flatMap
-> map
-> reduceByKey
-> sortBy
在这些转换中其中flatMap/map
、reduceByKey
、sotBy
都是转换算子,所有的转换算子都是Lazy
执行的。程序在遇到collect(Action 算子)
系统会触发job执行。Spark底层会按照RDD的依赖关系将整个计算拆分成若干个阶段,我们通常将RDD的依赖关系称为RDD的血统-lineage
。血统的依赖通常包含:宽依赖
、窄依赖
。
RDD容错
在理解DAGSchedule如何做状态划分的前提是需要大家了解一个专业术语lineage通常被人们称为RDD的血统。在了解什么是RDD的血统之前,先来看看程序猿进化过程。
上图中描述了一个程序猿起源变化的过程,我们可以近似的理解类似于RDD的转换也是一样的,Spark的计算本质就是对RDD做各种转换,因为RDD是一个不可变只读的集合,因此每次的转换都需要上一次的RDD作为本次转换的输入,因此RDD的lineage描述的是RDD间的相互依赖关系。为了保证RDD中数据的健壮性,RDD数据集通过所谓的血统关系(Lineage)记住了它是如何从其它RDD中演变过来的。Spark将RDD之间的关系归类为宽依赖和窄依赖。Spark会根据Lineage存储的RDD的依赖关系对RDD计算做故障容错,目前Saprk的容错策略更具RDD依赖关系重新计算、对RDD做Cache、对RDD做Checkpoint手段完成RDD计算的故障容错。
RDD 宽窄依赖
RDD在Lineage依赖方面分为两种Narrow Dependencies
与Wide Dependencies
用来解决数据容错的高效性。Narrow Dependencies
是指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区或多个父RDD的分区对应于子RDD的一个分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区。Wide Dependencies
父RDD的一个分区对应一个子RDD的多个分区。
对于Wide Dependencies这种计算的输入和输出在不同的节点上,一般需要夸节点做Shuffle,因此如果是RDD在做宽依赖恢复的时候需要多个节点重新计算成本较高。相对于Narrow Dependencies RDD间的计算是在同一个Task当中实现的是线程内部的的计算,因此在RDD分区数据丢失的的时候,也非常容易恢复。
Sage划分(重点)
Spark任务阶段的划分是按照RDD的lineage关系逆向生成的这么一个过程,Spark任务提交的流程大致如下图所示:
这里可以分析一下DAGScheduel中对State拆分的逻辑代码片段如下所示:
DAGScheduler.scala
第719
行
def runJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): Unit = {
val start = System.nanoTime
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
//...
}
DAGScheduler
-675
行
def submitJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): JobWaiter[U] = {
//eventProcessLoop 实现的是一个队列,系统底层会调用 doOnReceive -> case JobSubmitted -> dagScheduler.handleJobSubmitted(951行)
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, callSite, waiter,
SerializationUtils.clone(properties)))
waiter
}
DAGScheduler
-951
行
private[scheduler] def handleJobSubmitted(jobId: Int,
finalRDD: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
callSite: CallSite,
listener: JobListener,
properties: Properties) {
var finalStage: ResultStage = null
try {
//...
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
} catch {
//...
}
submitStage(finalStage)
}
DAGScheduler
-1060
行
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) {
logDebug("submitStage(" + stage + ")")
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
//计算当前State的父Stage
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: "