在《Spark任务调度概述》一节中介绍了Job的执行框架,也简单介绍了Job的概念。本文先介绍Job的基本概念,然后分析Job的创建和提交的实现原理,最后分析Job的执行状态的。
Job的基本概念
Job是Spark任务调度的顶层抽象,在执行Spark应用过程中,RDD的每个Action操作都会触发一个Job的创建和提交。
有两种类型的Job:
1.Result Job(结果Job)
此类Job是最常见的一种。每次执行Action操作时,就是创建的这一类的Job。该Job用来完成Action及依赖的相关数据的计算。
2.Map-Stage Job(映射阶段Job)
这一类Job主要用于SparkSQL的自适应查询计划中(adaptive query planning)。在提交后续Stage之前查看map操作的输出统计信息。它会在任何下游的Stage提交之前,为ShuffleMapStage计算map操作的输出。
另外要注意,Spark中Job都是通过ActiveJob类来实现的,而Job的类型通过ActiveJob类的finalStage字段的值来进行区分。
Job的创建
Job是提交给DAGScheduler来计算Action操作结果的顶层工作单元,它是在执行RDD的Action操作时产生的。RDD的每个Action操作都会调用SparkContext的runJob函数,而该函数会调用DAGScheduler的runJob函数来进行Job的创建和提交。
在实现层面,Job对应的是ActiveJob类。Job创建的整个过程的大体流程如图1所示:
图1 Job创建的基本流程
Job的实现类是ActiveJob,在创建ActiveJob对象时jobId和finalStage都已经生成。其中的jobId是一个自增的整数,每次创建新的job时,就会自动加1。jobId会作为FIFO资源调度算法的重要依据。若Job的类型是结果Job时,finalStage的类型是ResultStage,但它只包含最后一个Action动作而创建的Stage,但通过该Stage可以找到依赖的父Stage列表。
Job的创建代码如下:
val job = new ActiveJob(jobId, finalStage, callSite, listener,properties)
在后续的处理中,会把Job按RDD的shuffle依赖为边界,划分成多个Stage,Job和Stage的关系如图2所示:
图2 Job和Stage的关系
Job执行完成状态的获取
Job执行完成时如何得知其完成状态,如何处理Job的返回信息?其实,在提交Job时,会创建一个JobWaiter对象,该对象等待DAGScheduler提交的任务执行完成。当Job完成时,会把最终的状态信息保存到JobWaiter中。这个过程如图3所示:
图3 通过JobWaiter保存Job的完成状态信息
从图3可知,当任务终止或完成时,Worker端的执行后台服务会把任务的状态数据封装到消息StatusUpate中,并发送给Driver端的调度后台服务,调度后台服务通过TaskScheduler解析该状态信息,并发送对应的事件给DAGScheduler的事件处理框架来进行处理。
JobId和Task调度
前面提到过,每个Job都会使用一个唯一的JobId来进行标识,这是一个自整的整数,在同一个Spark应用中(相同SparkContext),每次创建新的Job时,就会在以前的JobId的基础上加1。通过JobId可以用来区分不同的Job。
另外,JobId也会作为FIFO调度的重要依据。
当使用FIFO调度算法时(默认Task调度算法),若同时有多个Task提交,会先比较Task的JobId的大小,JobId越小的Task会先执行。若Task的JobId相同,则会再比较StageId的值,根据StageId的值来决定Task执行的先后顺序。
Job的实现类:ActiveJob
该类代表一个在DAGScheduler运行的Job对象。该类的实现代码很简单,如下:
private[spark] class ActiveJob(
val jobId: Int,
val finalStage: Stage, // job对象中只保留最后一个Stage的引用
val callSite: CallSite,
val listener: JobListener, // Job执行完成后,由该对象来进行获取执行结果
val properties: Properties) {
// 分区数,不同类型的Job,获取分区数的方式不同
val numPartitions = finalStage match {
case r: ResultStage => r.partitions.length
case m: ShuffleMapStage => m.rdd.partitions.length
}
// 标识stage的那个分区已经计算完成了
val finished = Array.fill[Boolean](numPartitions)(false)
var numFinished = 0
}
下面介绍一个该类的各个成员的意义:
成员名 | 说明 |
---|---|
jobId | 每个Job都有一个唯一的id号来进行标识。这是一个自增的整数。 |
finalStage | 该Job计算的Stage,可以是:执行action的ResultStage;或submitMapStage的ShuffleMapStage。根据该字段把Job分为两类。 |
callSite | CallSite类型的实体,用户应用程序在这里进行Job的初始化。 |
listener | 监听Job结束或Job失败的事件,返回Job的执行状态信息。 |
properties | 与Job调度相关的属性 |
numPartitions | 需要为该Job计算的分区数量 |
finished | 是一个Boolean的数组,用来表示在该Job中计算完成的分区 |
numFinished | 在该Job中已经计算完成的分区数量 |
Job的实现类需要注意以下几点:
1.jobId在创建ActiveJob类对象之前就已经创建好了,该jobId是一个自增的整数。
2.finalStage是Job的最后一个Stage,若是通过Action操作触发Job的提交,则是一个ResultStage;若不是,则是一个ShuffleMapStage。
3.Job会记录finalStage已经计算完成的分区数,并通过计算完成的分区数来判断Job是否已经完成。那么为什么finalStage的分区数据计算完成了,job就完成了呢?这是因为在提交Stage(其实是提交该Stage对应的TaskSet)会先提交finalStage依赖的Stage,这样提交finalStage时,其实它依赖的Stage就已经全部执行完了。
另外,还需要注意,ActiveJob类中finalStage的分区数,其实是finalStage的Task数,每个Stage中有多少个分区,就会创建并提交多少个Task。
总结
本文分析Job的创建和提交的实现原理,并对其实现进行了分析。