原文地址:http://jerryshao.me/architecture/2013/04/21/Spark%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B-scheduler%E6%A8%A1%E5%9D%97/
Spark源码分析之-scheduler模块
Background
Spark在资源管理和调度方式上采用了类似于Hadoop YARN的方式,最上层是资源调度器,它负责分配资源和调度注册到Spark中的所有应用,Spark选用Mesos或是YARN等作为其资源调度框架。在每一个应用内部,Spark又实现了任务调度器,负责任务的调度和协调,类似于MapReduce。本质上,外层的资源调度和内层的任务调度相互独立,各司其职。本文对于Spark的源码分析主要集中在内层的任务调度器上,分析Spark任务调度器的实现。
Scheduler模块整体架构
scheduler
模块主要分为两大部分:
-
TaskSchedulerListener
。TaskSchedulerListener
部分的主要功能是监听用户提交的job,将job分解为不同的类型的stage以及相应的task,并向TaskScheduler
提交task。 -
TaskScheduler
。TaskScheduler
接收用户提交的task并执行。而TaskScheduler
根据部署的不同又分为三个子模块:ClusterScheduler
LocalScheduler
MesosScheduler
TaskSchedulerListener
Spark抽象了TaskSchedulerListener
并在其上实现了DAGScheduler
。DAGScheduler
的主要功能是接收用户提交的job,将job根据类型划分为不同的stage,并在每一个stage内产生一系列的task,向TaskScheduler
提交task。下面我们首先来看一下TaskSchedulerListener
部分的类图:
- 用户所提交的job在得到
DAGScheduler
的调度后,会被包装成ActiveJob
,同时会启动JobWaiter
阻塞监听job的完成状况。 - 于此同时依据job中
RDD
的dependency和dependency属性(NarrowDependency
,ShufflerDependecy
),DAGScheduler
会根据依赖关系的先后产生出不同的stage DAG(result stage, shuffle map stage)。 - 在每一个stage内部,根据stage产生出相应的task,包括
ResultTask
或是ShuffleMapTask
,这些task会根据RDD
中partition的数量和分布,产生出一组相应的task,并将其包装为TaskSet
提交到TaskScheduler
上去。
RDD的依赖关系和Stage的分类
在Spark中,每一个
RDD
是对于数据集在某一状态下的表现形式,而这个状态有可能是从前一状态转换而来的,因此换句话说这一个RDD
有可能与之前的RDD(s)
有依赖关系。根据依赖关系的不同,可以将RDD
分成两种不同的类型:Narrow Dependency
和Wide Dependency
。
Narrow Dependency
指的是child RDD
只依赖于parent RDD(s)
固定数量的partition。Wide Dependency
指的是child RDD
的每一个partition都依赖于parent RDD(s)
所有partition。它们之间的区别可参看下图:
根据
RDD
依赖关系的不同,Spark也将每一个job分为不同的stage,而stage之间的依赖关系则形成了DAG。对于Narrow Dependency
,Spark会尽量多地将RDD
转换放在同一个stage中;而对于Wide Dependency
,由于Wide Dependency
通常意味着shuffle操作,因此Spark会将此stage定义为ShuffleMapStage
,以便于向MapOutputTracker
注册shuffle操作。对于stage的划分可参看下图,Spark通常将shuffle操作定义为stage的边界。
DAGScheduler
在用户创建SparkContext
对象时,Spark会在内部创建DAGScheduler
对象,并根据用户的部署情况,绑定不同的TaskSechduler
,并启动DAGcheduler
private var taskScheduler: TaskScheduler = {
//...
}
taskScheduler.start()
private var dagScheduler = new DAGScheduler(taskScheduler)
dagScheduler.start()
而DAGScheduler
的启动会在内部创建daemon线程,daemon线程调用run()
从block queue中取出event进行处理。
而run()
会调用processEvent
来处理不同的event。
private def run() {
SparkEnv.set(env)
while (true) {
val event = eventQueue.poll(POLL_TIMEOUT, TimeUnit.MILLISECONDS)
if (event != null) {
logDebug("Got event of type " + event.getClass.getName)
}
if (event != null) {
if (processEvent(event)) {
return
}
}
val time = System.currentTimeMillis() // TODO: use a pluggable clock for testability
if (failed.size > 0 && time > lastFetchFailureTime + RESUBMIT_TIMEOUT) {
resubmitFailedStages()
} else {
submitWaitingStages()
}
}
}
DAGScheduler
处理的event包括:
JobSubmitted
CompletionEvent
ExecutorLost
TaskFailed
StopDAGScheduler
根据event的不同调用不同的方法去处理。
本质上DAGScheduler
是一个生产者-消费者模型,用户和TaskSchduler
产生event将其放入block queue,daemon线程消费event并处理相应事件。
Job的生与死
既然用户提交的job最终会交由DAGScheduler
去处理,那么我们就来研究一下DAGScheduler
处理job的整个流程。在这里我们分析两种不同类型的job的处理流程。
1.没有shuffle和reduce的job
val textFile = sc.textFile("README.md")
textFile.filter(line => line.contains("Spark")).count()
2.有shuffle和reduce的job
val textFile = sc.textFile("README.md")
textFile.flatMap(line => line.split(" ")).map(word => (word, 1)).reduceByKey((a, b) => a + b)
首先在对RDD
的count()
和reduceByKey()
操作都会调用SparkContext
的runJob()
来提交job,而SparkContext
的runJob()
最终会调用DAGScheduler
的runJob()
runJob()
会调用prepareJob()
对job进行预处理,封装成JobSubmitted
事件,放入queue中,并阻塞等待job完成
def runJob[T, U: ClassManifest](
finalRdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: String,
allowLocal: Boolean,
resultHandler: (Int, U) => Unit)
{
if (partitions.size == 0) {
return
}
val (toSubmit, waiter) = prepareJob(
finalRdd, func, partitions, callSite, allowLocal, resultHandler)
eventQueue.put(toSubmit)
waiter.awaitResult() match {
case JobSucceeded => {}
case JobFailed(exception: Exception) =>
logInfo("Failed to run " + callSite)
throw exception
}
}
当daemon线程的processEvent()
从queue中取出JobSubmitted
事件后,会根据job划分出不同的stage,并且提交stage:
首先,对于任何的job都会产生出一个finalStage
来产生和提交task。其次对于某些简单的job,它没有依赖关系,并且只有一个partition,这样的job会使用local thread处理而并非提交到TaskScheduler
上处理。
case JobSubmitted(finalRDD, func, partitions, allowLocal, callSite, listener) =>
val runId = nextRunId.getAndIncrement()
val finalStage = newStage(finalRDD, None, runId)
val job = new ActiveJob(runId, finalStage, func, partitions, callSite, listener)
clearCacheLocs()
if (allowLocal && finalStage.parents.size == 0 && partitions.length == 1) {
runLocally(job)
} else {
activeJobs += job
resultStageToJob(finalStage) = job
submitStage(finalStage)
}
其次,产生finalStage
后,需要调用submitStage()
,它根据stage之间的依赖关系得出stage DAG,并以依赖关系进行处理:
对于新提交的job,finalStage
的parent stage还未获得,因此submitStage
会调用getMissingParentStages()
来获得依赖关系:
这里parent stage是通过RDD
的依赖关系递归遍历获得。对于Wide Dependecy
也就是Shuffle Dependecy
,Spark会产生新的mapStage
作为finalStage
的parent,而对于Narrow Dependecy
Spark则不会产生新的stage。这里对stage的划分是按照上面提到的作为划分依据的,因此对于本段开头提到的两种job,第一种job只会产生一个finalStage
,而第二种job会产生finalStage
和mapStage
。
当stage DAG产生以后,针对每个stage需要产生task去执行,故在这会调用submitMissingTasks()
:
首先根据stage所依赖的RDD
的partition的分布,会产生出与partition数量相等的task,这些task根据partition的locality进行分布;其次对于finalStage
或是mapStage
会产生不同的task;最后所有的task会封装到TaskSet
内提交到TaskScheduler
去执行。
至此job在DAGScheduler
内的启动过程全部完成,交由TaskScheduler
执行task,当task执行完后会将结果返回给DAGScheduler
,DAGScheduler
调用handleTaskComplete()
处理task返回:
每个执行完成的task都会将结果返回给DAGScheduler
,DAGScheduler
根据返回结果来进行进一步的动作。
RDD的计算
RDD
的计算是在task中完成的。我们之前提到task分为ResultTask
和ShuffleMapTask
,我们分别来看一下这两种task具体的执行过程。
-
ResultTask
override def run(attemptId: Long): U = {
val context = new TaskContext(stageId, partition, attemptId)
try {
func(context, rdd.iterator(split, context))
} finally {
context.executeOnCompleteCallbacks()
}
}
-
ShuffleMapTask
ResultTask
和ShuffleMapTask
都会调用RDD
的iterator()
来计算和转换RDD
,不同的是:ResultTask
转换完RDD
后调用func()
计算结果;而ShufflerMapTask
则将其放入blockManager
中用来shuffle。
RDD
的计算调用iterator()
,iterator()
在内部调用compute()
从RDD
依赖关系的根开始计算:
至此大致分析了TaskSchedulerListener
,包括DAGScheduler
内部的结构,job生命周期内的活动,RDD
是何时何地计算的。接下来我们分析一下task在TaskScheduler
内干了什么。
TaskScheduler
前面也提到了Spark实现了三种不同的TaskScheduler
,包括LocalSheduler
、ClusterScheduler
和MesosScheduler
。LocalSheduler
是一个在本地执行的线程池,DAGScheduler
提交的所有task会在线程池中被执行,并将结果返回给DAGScheduler
。MesosScheduler
依赖于Mesos进行调度,笔者对Mesos了解甚少,因此不做分析。故此章节主要分析ClusterScheduler
模块。
ClusterScheduler
模块与deploy模块和executor模块耦合较为紧密,因此在分析ClUsterScheduler
时也会顺带介绍deploy和executor模块。
首先我们来看一下ClusterScheduler
的类图:
ClusterScheduler
的启动会伴随SparkDeploySchedulerBackend
的启动,而backend会将自己分为两个角色:首先是driver,driver是一个local运行的actor,负责与remote的executor进行通行,提交任务,控制executor;其次是StandaloneExecutorBackend
,Spark会在每一个slave node上启动一个StandaloneExecutorBackend
进程,负责执行任务,返回执行结果。
ClusterScheduler的启动
在SparkContext
实例化的过程中,ClusterScheduler
被随之实例化,同时赋予其SparkDeploySchedulerBackend
:
ClusterScheduler
的启动会启动SparkDeploySchedulerBackend
,同时启动daemon进程来检查speculative task:
SparkDeploySchedulerBacked
的启动首先会调用父类的start()
,接着它会启动client,并由client连接到master向每一个node的worker发送请求启动StandaloneExecutorBackend
。这里的client、master、worker涉及到了deploy模块,暂时不做具体介绍。而StandaloneExecutorBackend
则涉及到了executor模块,它主要的功能是在每一个node创建task可以运行的环境,并让task在其环境中运行。
在StandaloneSchedulerBackend
中会创建DriverActor
,它就是local的driver,以actor的方式与remote的executor进行通信。
在client实例化之前,会将StandaloneExecutorBackend
的启动环境作为参数传递给client,而client启动时会将此提交给master,由master分发给所有node上的worker,worker会配置环境并创建进程启动StandaloneExecutorBackend
。
至此ClusterScheduler
的启动,local driver的创建,remote executor环境的启动所有过程都已结束,ClusterScheduler
等待DAGScheduler
提交任务。
ClusterScheduler提交任务
DAGScheduler
会调用ClusterScheduler
提交任务,任务会被包装成TaskSetManager
并等待调度:
在任务提交的同时会启动定时器,如果任务还未被执行,定时器持续发出警告直到任务被执行。
同时会调用StandaloneSchedulerBackend
的reviveOffers()
,而它则会通过actor向driver发送ReviveOffers
,driver收到ReviveOffers
后调用makeOffers()
:
makeOffers()
会向ClusterScheduler
申请资源,并向executor提交LauchTask
请求。
接下来LaunchTask
会进入executor模块,StandaloneExecutorBackend
在收到LaunchTask
请求后会调用Executor
执行task:
Executor
内部是一个线程池,每一个提交的task都会包装为TaskRunner
交由threadpool执行:
其中task.run()
则真正执行了task中的任务,如前RDD的计算章节所述。返回值被包装成TaskResult
返回。
至此task在ClusterScheduler
内运行的流程有了一个大致的介绍,当然这里略掉了许多异常处理的分支,但这不影响我们对主线的了解。
END
至此对Spark的Scheduler模块的主线做了一个顺藤摸瓜式的介绍,Scheduler模块作为Spark最核心的模块之一,充分体现了Spark与MapReduce的不同之处,体现了Spark DAG思想的精巧和设计的优雅。
当然Spark的代码仍然在积极开发之中,当前的源码分析在过不久后可能会变得没有意义,但重要的是体会Spark区别于MapReduce的设计理念,以及DAG思想的应用。DAG作为对MapReduce框架的改进越来越受到大数据界的重视,hortonworks也提出了类似DAG的框架tez作为对MapReduce的改进。