任务计算源码剖析
理论指导
Spark在执行任务前期,会根据RDD的转换关系形成一个任务执行DAG。将任务划分成若干个stage。Spark底层在划分stage的依据是根据RDD间的依赖关系划分。Spark将RDD与RDD间的转换分类:ShuffleDependency-宽依赖
和NarrowDependency-窄依赖
,Spark如果发现RDD与RDD之间存在窄依赖关系,系统会自动将存在窄依赖关系的RDD计算算子归纳为一个stage,如果遇到宽依赖系统开启一个新的stage。
Spark宽窄依赖判断
- 宽依赖:父RDD的一个分区对应子RDD的多个分区,出现分叉就认定为宽依赖。
- 窄依赖:父RDD的1个分区(多个父RDD)仅仅只对应子RDD的一个分区认定为窄依赖。
Spark在任务提交前期,首先根据finalRDD逆推出所有依赖RDD,以及RDD间依赖关系,如果遇到窄依赖合并在当前的stage中,如果是宽依赖开启新的stage。
getMissingParentStages
将宽窄依赖进行划分。这一步体现了之前所说的如果是窄依赖就合并在当前的stage中,如果是宽依赖放到新的stage。
private def getMissingParentStages(stage: Stage): List[Stage] = {
val missing = new HashSet[Stage]
val visited = new HashSet[RDD[_]]
// We are manually maintaining a stack here to prevent StackOverflowError
// caused by recursively visiting
val waitingForVisit = new ArrayStack[RDD[_]]
def visit(rdd: RDD[_]) {
if (!visited(rdd)) {
visited += rdd
val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
if (rddHasUncachedPartitions) {
for (dep <- rdd.dependencies) {
dep match {
case shufDep: ShuffleDependency[_, _, _] =>
val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
if (!mapStage.isAvailable) {
missing += mapStage
}
case narrowDep: NarrowDependency[_] =>
waitingForVisit.push(narrowDep.rdd)
}
}
}
}
}
waitingForVisit.push(stage.rdd)
while (waitingForVisit.nonEmpty) {
visit(waitingForVisit.pop())
}
missing.toList
}
遇到宽依赖,系统会自动的创建一个
ShuffleMapStage
submitMissingTasks
这一步是划分stage后,交由submitMissingTasks进行任务的分区以及映射TaskSet等。
private def submitMissingTasks(stage: Stage, jobId: Int) {
logDebug("submitMissingTasks(" + stage + ")")
// First figure out the indexes of partition ids to compute.
//计算分区
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
...
//计算最佳位置
val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
stage match {
case s: ShuffleMapStage =>
partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap
case s: ResultStage =>
partitionsToCompute.map { id =>
val p = s.partitions(id)
(id, getPreferredLocs(stage.rdd, p))
}.toMap
}
} catch {
case NonFatal(e) =>
stage.makeNewStageAttempt(partitionsToCompute.size)
listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
runningStages -= stage
return
}
//将分区映射TaskSet
val tasks: Seq[Task[_]] = try {
val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
stage match {
case stage: ShuffleMapStage =>
stage.pendingPartitions.clear()
partitionsToCompute.map { id =>
val locs = taskIdToLocations(id)
val part = partitions(id)
stage.pendingPartitions += id
new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
}
case stage: ResultStage =>
partitionsToCompute.map { id =>
val p: Int = stage.partitions(id)
val part = partitions(p)
val locs = taskIdToLocations(id)
new ResultTask(stage.id, stage.latestInfo.attemptNumber,
taskBinary, part, locs, id, properties, serializedTaskMetrics,
Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
stage.rdd.isBarrier())
}
}
} catch {
case NonFatal(e) =>
abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
runningStages -= stage
return
}
//调用taskScheduler #submitTasks TaskSet
if (tasks.size > 0) {
logInfo(s"Submitting ${tasks.size} missing tasks from $stage (${stage.rdd}) (first 15 " +
s"tasks are for partitions ${tasks.take(15).map(_.partitionId)})")
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))
} else {
// Because we posted SparkListenerStageSubmitted earlier, we should mark
// the stage as completed here in case there are no tasks to run
markStageAsFinished(stage, None)
stage match {
case stage: ShuffleMapStage =>
logDebug(s"Stage ${stage} is actually done; " +
s"(available: ${stage.isAvailable}," +
s"available outputs: ${stage.numAvailableOutputs}," +
s"partitions: ${stage.numPartitions})")
markMapStageJobsAsFinished(stage)
case stage : ResultStage =>
logDebug(s"Stage ${stage} is actually done; (partitions: ${stage.numPartitions})")
}
submitWaitingChildStages(stage)
}
...
总结关键字:逆推、finalRDD、ResultStage、ShuffleMapStage、ShuffleMapTask、ResultTask、ShuffleDependency、NarrowDependency、DAGScheduler、TaskScheduler、SchedulerBacked、DAGSchedulerEventProcessLoop
个人总结:SparkContext是由两个调度完成的,DAGScheduler
和TaskScheduler
,其中DAGScheduler
负责stage的划分(相当于谋士一样,来计划谋略),而TaskScheduler
是负责任务的资源计算、映射TaskSet、任务提交等。
TaskScheduler
内部握有SchedulerBackend
,是负责提交任务后,去与executor进程进行连接,以便执行任务。
stage会分为ResultStage(最终的stage)
和ShuffleMapStage
,它们分别对应着Task
中的ResultTask
和ShuffleMapTask
。
在DAGScheduler
执行过程中有一个DAGSchedulerEventProcessLoop
,它的内部实现是一个队列,是DAG调度程序的主事件循环。
在这之后根据finalRDD逆推出所有依赖RDD,以及RDD间依赖关系,如果遇到窄依赖(NarrowDependency)合并在当前的stage中,如果是宽依赖(ShuffleDependency)开启新的stage。
资料总结:
job提交之后,调用runjob,到最终task被分配到executor之前所涉及到的调度相关
1.首先涉及到的调度是job stage 划分和提交过程,也就是submitStage方法,所有又依赖的Satge,也就是说有父Satge的子Stage,子Stage调用submitSatge的时候,会将子Satge添加到watingSatge队列中,换句话说,如果一个Stage有父依赖,那么他就不能被subnitMissingSatge submit,会被加入到watingSatge,只有没有依赖的Satge才会被提交。
没有依赖的Stage提交,会将Satge转换成tasksetManager,提交给TaskScheduar
2.taskSchedular在初始化的时候,方法位于sparkcontext中,初始化的时候初始化了一个队列,这个队列有两个选择:FIFO/FAIR,
tasksetManager提交给taskSchedular的时候就会加入到该队列中,比如FIFO队列,有两层排序,一层是根据jobid,jobid越小的优先级越高,同一job内部,存在第二层排序,stageid,stageid越小的优先级越高
值得注意的一点就是:stage提交的时候,有依赖,就不会添加到队列中,会加入到watingSatge中,等待某一个stage完成之后,会检查watingSatge提交已经没有依赖的Stage。