Spark技术内幕:Stage划分及提交源码分析
http://blog.csdn.net/anzhsoft/article/details/39859463
当触发一个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去运行的。
在上文《Spark技术内幕:Stage划分及提交源码分析》中,我们分析了Stage的生成和提交。但是Stage的提交,只是DAGScheduler完成了对DAG的划分,生成了一个计算拓扑,即需要按照顺序计算的Stage,Stage中包含了可以以partition为单位并行计算的Task。我们并没有分析Stage中得Task是如何生成并且最终提交到Executor中去的。
这就是本文的主题。
从org.apache.spark.scheduler.DAGScheduler#submitMissingTasks开始,分析Stage是如何生成TaskSet的。
如果一个Stage的所有的parent stage都已经计算完成或者存在于cache中,那么他会调用submitMissingTasks来提交该Stage所包含的Tasks。
org.apache.spark.scheduler.DAGScheduler#submitMissingTasks的计算流程如下:
- 首先得到RDD中需要计算的partition,对于Shuffle类型的stage,需要判断stage中是否缓存了该结果;对于Result类型的Final Stage,则判断计算Job中该partition是否已经计算完成。
- 序列化task的binary。Executor可以通过广播变量得到它。每个task运行的时候首先会反序列化。这样在不同的executor上运行的task是隔离的,不会相互影响。
- 为每个需要计算的partition生成一个task:对于Shuffle类型依赖的Stage,生成ShuffleMapTask类型的task;对于Result类型的Stage,生成一个ResultTask类型的task
- 确保Task是可以被序列化的。因为不同的cluster有不同的taskScheduler,在这里判断可以简化逻辑;保证TaskSet的task都是可以序列化的
- 通过TaskScheduler提交TaskSet。
- private[spark] class TaskSet(
- val tasks: Array[Task[_]],
- val stageId: Int,
- val attempt: Int,
- val priority: Int,
- val properties: Properties) {
- val id: String = stageId + "." + attempt
- override def toString: String = "TaskSet " + id
- }
- org.apache.spark.scheduler.TaskSchedulerImpl#submitTasks
- org.apache.spark.scheduler.SchedulableBuilder#addTaskSetManager
- org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend#reviveOffers
- org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverActor#makeOffers
- org.apache.spark.scheduler.TaskSchedulerImpl#resourceOffers
- org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverActor#launchTasks
- org.apache.spark.executor.CoarseGrainedExecutorBackend.receiveWithLogging#launchTask
- org.apache.spark.executor.Executor#launchTask
- def launchTask(
- context: ExecutorBackend, taskId: Long, taskName: String, serializedTask: ByteBuffer) {
- val tr = new TaskRunner(context, taskId, taskName, serializedTask)
- runningTasks.put(taskId, tr)
- threadPool.execute(tr) // 开始在executor中运行
- }
- final def run(attemptId: Long): T = {
- context = new TaskContext(stageId, partitionId, attemptId, runningLocally = false)
- context.taskMetrics.hostname = Utils.localHostName()
- taskThread = Thread.currentThread()
- if (_killed) {
- kill(interruptThread = false)
- }
- runTask(context)
- }
对于原来提到的两种Task,即
- org.apache.spark.scheduler.ShuffleMapTask
- org.apache.spark.scheduler.ResultTask
- override def runTask(context: TaskContext): U = {
- // Deserialize the RDD and the func using the broadcast variables.
- val ser = SparkEnv.get.closureSerializer.newInstance()
- val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
- ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
- metrics = Some(context.taskMetrics)
- try {
- func(context, rdd.iterator(partition, context))
- } finally {
- context.markTaskCompleted()
- }
- }
- override def runTask(context: TaskContext): MapStatus = {
- // Deserialize the RDD using the broadcast variable.
- val ser = SparkEnv.get.closureSerializer.newInstance()
- val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
- ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
- //此处的taskBinary即为在org.apache.spark.scheduler.DAGScheduler#submitMissingTasks序列化的task的广播变量取得的
- metrics = Some(context.taskMetrics)
- var writer: ShuffleWriter[Any, Any] = null
- try {
- val manager = SparkEnv.get.shuffleManager
- writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
- writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]]) // 将rdd计算的结果写入memory或者disk
- return writer.stop(success = true).get
- } catch {
- case e: Exception =>
- if (writer != null) {
- writer.stop(success = false)
- }
- throw e
- } finally {
- context.markTaskCompleted()
- }
- }
Spark技术内幕:Stage划分及提交源码分析的更多相关文章
- Spark技术内幕: Task向Executor提交的源码解析
在上文<Spark技术内幕:Stage划分及提交源码分析>中,我们分析了Stage的生成和提交.但是Stage的提交,只是DAGScheduler完成了对DAG的划分,生成了一个计算拓扑, ...
- Spark源码剖析(八):stage划分原理与源码剖析
引言 对于Spark开发人员来说,了解stage的划分算法可以让你知道自己编写的spark application被划分为几个job,每个job被划分为几个stage,每个stage包括了你的哪些代码 ...
- mysql复制那点事(2)-binlog组提交源码分析和实现
mysql复制那点事(2)-binlog组提交源码分析和实现 [TOC] 0. 参考文献 序号 文献 1 MySQL 5.7 MTS源码分析 2 MySQL 组提交 3 MySQL Redo/Binl ...
- Spark技术内幕: Task向Executor提交的源代码解析
在上文<Spark技术内幕:Stage划分及提交源代码分析>中,我们分析了Stage的生成和提交.可是Stage的提交,仅仅是DAGScheduler完毕了对DAG的划分,生成了一个计算拓 ...
- 66、Spark Streaming:数据处理原理剖析与源码分析(block与batch关系透彻解析)
一.数据处理原理剖析 每隔我们设置的batch interval 的time,就去找ReceiverTracker,将其中的,从上次划分batch的时间,到目前为止的这个batch interval ...
- 🏆【Alibaba微服务技术系列】「Dubbo3.0技术专题」回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)
RPC服务 什么叫RPC? RPC[Remote Procedure Call]是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范.它允许程序调用另一个地址空间(通常是共享网络的另 ...
- Hadoop-1.2.1学习之Job创建和提交源码分析
在Hadoop中,MapReduce的Java作业通常由编写Mapper和Reducer開始.接着创建Job对象.然后使用该对象的set方法设置Mapper和Reducer以及诸如输入输出等參数,最后 ...
- MapReduce任务提交源码分析
为了测试MapReduce提交的详细流程.需要在提交这一步打上断点: F7进入方法: 进入submit方法: 注意这个connect方法,它在连接谁呢?我们知道,Driver是作为客户端存在的,那么客 ...
- 小记--------spark的Master的Application注册机制源码分析及Master的注册机制原理分析
原理图解: Master类位置所在:spark-core_2.11-2.1.0.jar的org.apache.spark.deploy.master下的Master类 //截取了部分代码 //处理 ...
随机推荐
- 【原创】lua编译时发现缺少readline库
编译lualua项目,其中用到了lua-5.1版本的源码,编译时提示缺少readline库,找不到readline/readline.h头文件等 发现系统中其实有安装readline库不过没有做链接和 ...
- org.springframework.web.filter.DelegatingFilterProxy的理解
org.springframework.web.filter.DelegatingFilterProxy可以将filter交给spring管理. 我们web.xml中配置filter时一般采用下面这种 ...
- 如何使用 SPICE client (virt-viewer) 来连接远程虚拟机桌面?
如何使用 SPICE client (virt-viewer) 来连接远程虚拟机桌面? 什么是SPICE? SPICE (Simple Protocol for Independent Computi ...
- Java语言速览:StackOverflow
关于 java Java(请不要与 JavaScript 搞混)是一种设计为与 Java 虚拟机 (JVM) 一起使用的多用途编程语言.一般将安装了相关工具可以开发并运行 Java 程序的电脑系统称为 ...
- BNUOJ 26475 Cookie Selection
LINK:BNUOJ 26475 Cookie Selection 题意: 你在不停的输入数字a1,a2,a3,......,ak,当你输入#时,就把已输入数字中的第k/2+1删除,然后剩下的数字又组 ...
- ECSHOP模板文件说明
最新ECSHOP 2.7.3完整的一套ECSHOP模板有一下构造 • 35个 .dwt文件(模板框架文件.可调用lbi库文件的主体文件) • 57个 .lbi文件(模板库文件,可通过后台库项目管理直接 ...
- JS单词形式的运算符
1.void 运算表达式并忽略其返回值,比如void (1+2),void (0) <html> <head> <meta http-equiv="conten ...
- Android开发日志问题
以前在Android开发中发现,日志打印好多,调试的时候各种加Log,之后就各种不删除,导致项目后期花大把时间删除日志打印. 学到一个好方法: 在所有尽可能高的父类里面加上一个常量 DEBUG ,一开 ...
- C#实现在Winform中嵌入Word和Excel
http://www.cnblogs.com/wuzi145/archive/2012/05/08/2490680.html 在此只是介绍一个简单控件:dsoframer.ocx的使用,这个控件需要通 ...
- TortoiseGit disconnected: no supported authentication methods available(server sent:publickey)
之前一直用命令行,现在想用图形工具,TortoiseGit,安装后遇到错误 TortoiseGit disconnected: no supported authentication methods ...