当触发一个RDD的action后。以count为例,调用关系例如以下:
- org.apache.spark.rdd.RDD#count
- org.apache.spark.SparkContext#runJob
- org.apache.spark.scheduler.DAGScheduler#runJob
- org.apache.spark.scheduler.DAGScheduler#submitJob
- org.apache.spark.scheduler.DAGSchedulerEventProcessActor#receive(JobSubmitted)
- org.apache.spark.scheduler.DAGScheduler#handleJobSubmitted
当中步骤五的DAGSchedulerEventProcessActor是DAGScheduler 的与外部交互的接口代理,DAGScheduler在创建时会创建名字为eventProcessActor的actor。这个actor的作用看它的实现就一目了然了:
/** * The main event loop of the DAG scheduler. */ def receive = { case JobSubmitted(jobId, rdd, func, partitions, allowLocal, callSite, listener, properties) => dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, allowLocal, callSite, listener, properties) // 提交job。来自与RDD->SparkContext->DAGScheduler的消息。之所以在这须要在这里中转一下,是为了模块功能的一致性。 case StageCancelled(stageId) => // 消息源org.apache.spark.ui.jobs.JobProgressTab,在GUI上显示一个SparkContext的Job的执行状态。 // 用户能够cancel一个Stage。会通过SparkContext->DAGScheduler 传递到这里。 dagScheduler.handleStageCancellation(stageId) case JobCancelled(jobId) => // 来自于org.apache.spark.scheduler.JobWaiter的消息。取消一个Job dagScheduler.handleJobCancellation(jobId) case JobGroupCancelled(groupId) => // 取消整个Job Group dagScheduler.handleJobGroupCancelled(groupId) case AllJobsCancelled => //取消全部Job dagScheduler.doCancelAllJobs() case ExecutorAdded(execId, host) => // TaskScheduler得到一个Executor被加入的消息。详细来自org.apache.spark.scheduler.TaskSchedulerImpl.resourceOffers dagScheduler.handleExecutorAdded(execId, host) case ExecutorLost(execId) => //来自TaskScheduler dagScheduler.handleExecutorLost(execId) case BeginEvent(task, taskInfo) => // 来自TaskScheduler dagScheduler.handleBeginEvent(task, taskInfo) case GettingResultEvent(taskInfo) => //处理获得TaskResult信息的消息 dagScheduler.handleGetTaskResult(taskInfo) case completion @ CompletionEvent(task, reason, _, _, taskInfo, taskMetrics) => //来自TaskScheduler,报告task是完毕或者失败 dagScheduler.handleTaskCompletion(completion) case TaskSetFailed(taskSet, reason) => //来自TaskScheduler,要么TaskSet失败次数超过阈值或者因为Job Cancel。 dagScheduler.handleTaskSetFailed(taskSet, reason) case ResubmitFailedStages => //当一个Stage处理失败时,重试。来自org.apache.spark.scheduler.DAGScheduler.handleTaskCompletion dagScheduler.resubmitFailedStages() }
总结一下org.apache.spark.scheduler.DAGSchedulerEventProcessActor的作用:能够把他理解成DAGScheduler的对外的功能接口。
它对外隐藏了自己内部实现的细节。也更易于理解其逻辑;也减少了维护成本,将DAGScheduler的比較复杂功能接口化。
handleJobSubmitted
org.apache.spark.scheduler.DAGScheduler#handleJobSubmitted首先会依据RDD创建finalStage。
finalStage,顾名思义,就是最后的那个Stage。
然后创建job,最后提交。
提交的job假设满足一下条件。那么它将以本地模式执行:
1)spark.localExecution.enabled设置为true 而且 2)用户程序显式指定能够本地执行 而且 3)finalStage的没有父Stage 而且 4)仅有一个partition
3)和 4)的话主要为了任务能够高速执行。假设有多个stage或者多个partition的话,本地执行可能会因为本机的计算资源的问题而影响任务的计算速度。
要理解什么是Stage。首先要搞明确什么是Task。Task是在集群上执行的基本单位。一个Task负责处理RDD的一个partition。RDD的多个patition会分别由不同的Task去处理。
当然了这些Task的处理逻辑全然是一致的。
这一组Task就组成了一个Stage。有两种Task:
- org.apache.spark.scheduler.ShuffleMapTask
- org.apache.spark.scheduler.ResultTask
ShuffleMapTask依据Task的partitioner将计算结果放到不同的bucket中。而ResultTask将计算结果发送回Driver Application。一个Job包括了多个Stage,而Stage是由一组全然同样的Task组成的。
最后的Stage包括了一组ResultTask。
在用户触发了一个action后,比方count,collect。SparkContext会通过runJob的函数開始进行任务提交。最后会通过DAG的event processor 传递到DAGScheduler本身的handleJobSubmitted,它首先会划分Stage,提交Stage,提交Task。
至此,Task就開始在执行在集群上了。
一个Stage的開始就是从外部存储或者shuffle结果中读取数据;一个Stage的结束就是因为发生shuffle或者生成结果时。
创建finalStage
handleJobSubmitted 通过调用newStage来创建finalStage:
finalStage = newStage(finalRDD, partitions.size, None, jobId, callSite)
创建一个result stage。或者说finalStage,是通过调用org.apache.spark.scheduler.DAGScheduler#newStage完毕的。而创建一个shuffle stage,须要通过调用org.apache.spark.scheduler.DAGScheduler#newOrUsedStage。
private def newStage( rdd: RDD[_], numTasks: Int, shuffleDep: Option[ShuffleDependency[_, _, _]], jobId: Int, callSite: CallSite) : Stage = { val id = nextStageId.getAndIncrement() val stage = new Stage(id, rdd, numTasks, shuffleDep, getParentStages(rdd, jobId), jobId, callSite) stageIdToStage(id) = stage updateJobIdStageIdMaps(jobId, stage) stage }
对于result 的final stage来说。传入的shuffleDep是None。
我们知道。RDD通过org.apache.spark.rdd.RDD#getDependencies能够获得它依赖的parent RDD。
而Stage也可能会有parent Stage。
看一个RDD论文的Stage划分吧:
一个stage的边界。输入是外部的存储或者一个stage shuffle的结果;输入则是Job的结果(result task相应的stage)或者shuffle的结果。
上图的话stage3的输入则是RDD A和RDD F shuffle的结果。
而A和F因为到B和G须要shuffle,因此须要划分到不同的stage。
从源代码实现的角度来看。通过触发action也就是最后一个RDD创建final stage(上图的stage 3),我们注意到new Stage的第五个參数就是该Stage的parent Stage:通过rdd和job id获取:
// 生成rdd的parent Stage。没遇到一个ShuffleDependency。就会生成一个Stage private def getParentStages(rdd: RDD[_], jobId: Int): List[Stage] = { val parents = new HashSet[Stage] //存储parent stage val visited = new HashSet[RDD[_]] //存储已经被訪问到得RDD // We are manually maintaining a stack here to prevent StackOverflowError // caused by recursively visiting // 存储须要被处理的RDD。Stack中得RDD都须要被处理。val waitingForVisit = new Stack[RDD[_]] def visit(r: RDD[_]) { if (!visited(r)) { visited += r // Kind of ugly: need to register RDDs with the cache here since // we can't do it in its constructor because # of partitions is unknown for (dep <- r.dependencies) { dep match { case shufDep: ShuffleDependency[_, _, _] => // 在ShuffleDependency时须要生成新的stage parents += getShuffleMapStage(shufDep, jobId) case _ => waitingForVisit.push(dep.rdd) //不是ShuffleDependency,那么就属于同一个Stage } } } } waitingForVisit.push(rdd) // 输入的rdd作为第一个须要处理的RDD。
然后从该rdd開始。顺序訪问其parent rdd while (!waitingForVisit.isEmpty) { //仅仅要stack不为空。则一直处理。 visit(waitingForVisit.pop()) //每次visit假设遇到了ShuffleDependency,那么就会形成一个Stage,否则这些RDD属于同一个Stage } parents.toList }
生成了finalStage后。就须要提交Stage了。
// 提交Stage,假设有parent Stage没有提交,那么递归提交它。 private def submitStage(stage: Stage) { val jobId = activeJobForStage(stage) if (jobId.isDefined) { logDebug("submitStage(" + stage + ")") // 假设当前stage不在等待其parent stage的返回。而且 不在执行的状态, 而且 没有已经失败(失败会有重试机制,不会通过这里再次提交) if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) { val missing = getMissingParentStages(stage).sortBy(_.id) logDebug("missing: " + missing) if (missing == Nil) { // 假设全部的parent stage都已经完毕,那么提交该stage所包括的task logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents") submitMissingTasks(stage, jobId.get) } else { for (parent <- missing) { // 有parent stage为完毕,则递归提交它 submitStage(parent) } waitingStages += stage } } } else { abortStage(stage, "No active job for stage " + stage.id) } }
DAGScheduler将Stage划分完毕后,提交实际上是通过把Stage转换为TaskSet,然后通过TaskScheduler将计算任务终于提交到集群。
其所在的位置例如以下图所看到的。
接下来,将分析Stage是怎样转换为TaskSet,并终于提交到Executor去执行的。
BTW。近期工作太忙了,基本上到家洗漱完都要10点多。
也再没有精力去进行源代码解析了。幸运的是周末不用加班。因此以后的博文更新都要集中在周末了。加油。