[Spark源代码剖析] DAGScheduler划分stage
转载请标明出处:http://blog.csdn.net/bigbigdata/article/details/47293263
本文基于Spark 1.3.1
先上一些stage相关的知识点:
- DAGScheduler将Job分解成具有前后依赖关系的多个stage
- DAGScheduler是依据ShuffleDependency划分stage的
- stage分为ShuffleMapStage和ResultStage。一个Job中包括一个ResultStage及多个ShuffleMapStage
- 一个stage包括多个tasks,task的个数即该stage的finalRDD的partition数
- 一个stage中的task全然同样,ShuffleMapStage包括的都是ShuffleMapTask;ResultStage包括的都是ResultTask
下图为整个划分stage的函数调用关系图
在DAGScheduler内部通过post一个JobSubmitted事件来触发Job的提交
DAGScheduler.eventProcessLoop.post( JobSubmitted(...) )
DAGScheduler.handleJobSubmitted
既然这两个方法都是DAGScheduler内部的实现,为什么不直接调用函数而要这样“多此一举”呢?我猜想这是为了保证整个系统事件模型的完整性。
DAGScheduler.handleJobSubmitted部分源代码及例如以下
private[scheduler] def handleJobSubmitted(jobId: Int,
finalRDD: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
allowLocal: Boolean,
callSite: CallSite,
listener: JobListener,
properties: Properties) {
var finalStage: ResultStage = null
try {
//< 创建finalStage可能会抛出一个异常, 比方, jobs是基于一个HadoopRDD的但这个HadoopRDD已被删除
finalStage = newResultStage(finalRDD, partitions.size, jobId, callSite)
} catch {
case e: Exception =>
return
}
//< 此处省略n行代码
}
该函数通过调用newResultStage函数来创建唯一的ResultStage,也就是finalStage。调用newResultStage时,传入了finalRDD、partitions.size等參数。
跟进到DAGScheduler.newResultStage
private def newResultStage(
rdd: RDD[_],
numTasks: Int,
jobId: Int,
callSite: CallSite): ResultStage = {
val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId)
val stage: ResultStage = new ResultStage(id, rdd, numTasks, parentStages, jobId, callSite)
stageIdToStage(id) = stage
updateJobIdStageIdMaps(jobId, stage)
stage
}
DAGScheduler.newResultStage首先调用val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId)。这个调用看起来像是要先确定好该ResultStage依赖的父stages,
问题1:那么是直接父stage呢?还是父stage及间接依赖的全部父stage呢?记住这个问题,继续往下看。
跟进到DAGScheduler.getParentStagesAndId:
private def getParentStagesAndId(rdd: RDD[_], jobId: Int): (List[Stage], Int) = {
val parentStages = getParentStages(rdd, jobId)
val id = nextStageId.getAndIncrement() //< 这个调用确定了每一个stage的id,划分stage时。会从右到左,由于是递归调用,事实上越左的stage创建时。越早调到?try
(parentStages, id)
}
该函数调用getParentStages获得parentStages,之后获取一个递增的id。连同刚获得的parentStages一同返回。并在newResultStage中,将id作为ResultStage的id。
那么。
问题2:stage id是父stage的大还是子stage的大?
继续跟进源代码,全部提问均会在后面解答。跟到getParentStages里
//< 这个函数的实现方式比較巧妙
private def getParentStages(rdd: RDD[_], jobId: Int): List[Stage] = {
//< 通过vist一级一级vist得到的父stage
val parents = new HashSet[Stage]
//< 已经visted的rdd
val visited = new HashSet[RDD[_]]
val waitingForVisit = new Stack[RDD[_]]
def visit(r: RDD[_]) {
if (!visited(r)) {
visited += r
for (dep <- r.dependencies) {
dep match {
//< 若为宽依赖,调用getShuffleMapStage
case shufDep: ShuffleDependency[_, _, _] =>
parents += getShuffleMapStage(shufDep, jobId)
case _ =>
//< 若为窄依赖。将该依赖中的rdd增加到待vist列表。以保证能一级一级往上vist,直至遍历整个DAG图
waitingForVisit.push(dep.rdd)
}
}
}
}
waitingForVisit.push(rdd)
while (waitingForVisit.nonEmpty) {
visit(waitingForVisit.pop())
}
parents.toList
}
函数getParentStages中。遍历整个RDD依赖图的finalRDD的List[dependency] (关于RDD及依赖。可參考举例说明Spark RDD的分区、依赖),若遇到ShuffleDependency(即宽依赖),则调用getShuffleMapStage(shufDep, jobId)返回一个ShuffleMapStage类型对象,增加到父stage列表中。
若为NarrowDenpendency,则将该NarrowDenpendency包括的RDD增加到待visit队列中。之后继续遍历待visit队列中的RDD,直到遇到ShuffleDependency或无依赖的RDD。
函数getParentStages的职责说白了就是:以參数rdd为起点。一级一级遍历依赖。碰到窄依赖就继续往前遍历,碰到宽依赖就调用getShuffleMapStage(shufDep, jobId)。
这里须要特别注意的是。getParentStages以rdd为起点遍历RDD依赖并不会遍历整个RDD依赖图。而是一级一级遍历直到全部“遍历路线”都碰到了宽依赖就停止。
剩下的事。在遍历的过程中交给getShuffleMapStage。
那么。让我来看看函数getShuffleMapStage的实现:
private def getShuffleMapStage(
shuffleDep: ShuffleDependency[_, _, _],
jobId: Int): ShuffleMapStage = {
shuffleToMapStage.get(shuffleDep.shuffleId) match {
case Some(stage) => stage
case None =>
// We are going to register ancestor shuffle dependencies
registerShuffleDependencies(shuffleDep, jobId)
//< 然后创建新的ShuffleMapStage
val stage = newOrUsedShuffleStage(shuffleDep, jobId)
shuffleToMapStage(shuffleDep.shuffleId) = stage
stage
}
}
在划分stage的过程中,由于每次shuffleDep.shuffleId都不同且都是第一次出现,显然shuffleToMapStage.get(shuffleDep.shuffleId)会match到None。便会调用newOrUsedShuffleStage。来看看它的实现:
private def registerShuffleDependencies(shuffleDep: ShuffleDependency[_, _, _], jobId: Int) {
val parentsWithNoMapStage = getAncestorShuffleDependencies(shuffleDep.rdd)
while (parentsWithNoMapStage.nonEmpty) {
//< 出栈的事实上是shuffleDep的前一个宽依赖,且shuffleToMapStage不包括以该出栈宽依赖id为key的元素
val currentShufDep = parentsWithNoMapStage.pop()
//< 创建新的ShuffleMapStage
val stage = newOrUsedShuffleStage(currentShufDep, jobId)
//< 将新创建的ShuffleMapStage增加到shuffleId -> ShuffleMapStage映射关系中
shuffleToMapStage(currentShufDep.shuffleId) = stage
}
}
函数registerShuffleDependencies首先调用getAncestorShuffleDependencies,这个函数遍历參数rdd的List[dependency],若遇到ShuffleDependency,增加到parents: Stack[ShuffleDependency[_, _, _]]中。若遇到窄依赖,则遍历该窄依赖相应rdd的父一层依赖,知道遇到宽依赖为止。
实现与getParentStages基本一致,不同的是这里是将宽依赖增加到parents中并返回。
registerShuffleDependencies拿到各个“依赖路线”近期的全部宽依赖后。
对每一个宽依赖调用newOrUsedShuffleStage。该函数用来创建新ShuffleMapStage或获得已经存在的ShuffleMapStage。来看它的实现:
private def newOrUsedShuffleStage(
shuffleDep: ShuffleDependency[_, _, _],
jobId: Int): ShuffleMapStage = {
val rdd = shuffleDep.rdd
val numTasks = rdd.partitions.size
val stage = newShuffleMapStage(rdd, numTasks, shuffleDep, jobId, rdd.creationSite)
//< 若该shuffleDep.shulleId相应的, stage已经在MapOutputTracker中存在,那么可用的输出的数量及位置将从MapOutputTracker恢复
if (mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) {
val serLocs = mapOutputTracker.getSerializedMapOutputStatuses(shuffleDep.shuffleId)
val locs = MapOutputTracker.deserializeMapStatuses(serLocs)
for (i <- 0 until locs.size) {
stage.outputLocs(i) = Option(locs(i)).toList // locs(i) will be null if missing
}
stage.numAvailableOutputs = locs.count(_ != null)
} else {
// Kind of ugly: need to register RDDs with the cache and map output tracker here
// since we can't do it in the RDD constructor because # of partitions is unknown
logInfo("Registering RDD " + rdd.id + " (" + rdd.getCreationSite + ")")
//< 否则使用shuffleDep.shuffleId, rdd.partitions.size在mapOutputTracker中注冊,这会在shuffle阶段reducer从shuffleMap端fetch数据起作用
mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.size)
}
stage
}
函数newOrUsedShuffleStage首先调用newShuffleMapStage来创建新的ShuffleMapStage。来看下newShuffleMapStage的实现:
private def newShuffleMapStage(
rdd: RDD[_],
numTasks: Int,
shuffleDep: ShuffleDependency[_, _, _],
jobId: Int,
callSite: CallSite): ShuffleMapStage = {
val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId)
val stage: ShuffleMapStage = new ShuffleMapStage(id, rdd, numTasks, parentStages,
jobId, callSite, shuffleDep)
stageIdToStage(id) = stage
updateJobIdStageIdMaps(jobId, stage)
stage
}
结合文章開始处的函数调用关系图,能够看到newShuffleMapStage居然又调用getParentStagesAndId来获取它的parentStages。那么,文章开头处的整个函数调用流程又会继续走一遍。不同的是起点rdd不是原来的finalRDD而是变成了这里的宽依赖的rdd。
静下心来,细致看几遍上文提到的源代码及凝视,事实上每一次的如上图所看到的的递归调用。事实上就仅仅做了两件事:
- 遍历起点RDD的依赖列表。若遇到窄依赖,则继续遍历该窄依赖的父List[RDD]的依赖,直到碰到宽依赖。若碰到宽依赖(不管是起点RDD的宽依赖还是遍历多级依赖碰到的宽依赖),则以宽依赖RDD为起点再次反复上述过程。直到到达RDD依赖图的最左端。也就是遍历到了没有依赖的RDD。则进入2
- 达到RDD依赖图的最左端,即递归调用也到了最深得层数,
getParentStagesAndId中,getParentStages第一次返回(第一次返回为空,由于最初的stage没有父stage),val id = nextStageId.getAndIncrement()也是第一次被调用,获得第一个stage的id,为0(注意。这个时候还没有创建第一个stage)。这之后,便调用
val stage: ShuffleMapStage = new ShuffleMapStage(id, rdd, numTasks, parentStages, jobId, callSite, shuffleDep)
创建第一个ShuffleMapStage。至此。这一层递归调用结束,返回到上一层递归中,这一层创建的全部的ShuffleMapStage会作为下一层stage的父List[stage]用来构造上一层的stages。上一层递归调用返回后,上一层创建的stage又将作为上上层的parent List[stage]来构造上上层的stages。依次类推。直到最后的ResultStage也被创建出来为止。整个stage的划分完毕。
有一个须要注意的点是,不管对于ShuffleMapStage还是ResultStage来说,task的个数即该stage的finalRDD的partition的个数,细致查看下上文中的newResultStage和newShuffleMapStage函数能够搞明确这点。不再赘述。
最后,解答下上文中的两个问题:
- 问题1:每一个stage都有一个val parents: List[Stage]成员,保存的是其直接依赖的父stages;其直接父stages又有自身依赖的父stages,以此类推,构成了整个DAG图
- 问题2:父stage的id比子stage的id大,DAG图中。越左边的stage,id越小。
[Spark源代码剖析] DAGScheduler划分stage的更多相关文章
- [Spark源代码剖析] DAGScheduler提交stage
转载请标明出处:http://blog.csdn.net/bigbigdata/article/details/47310657 DAGScheduler通过调用submitStage来提交stage ...
- Spark源码阅读(1): Stage划分
Spark中job由action动作生成,那么stage是如何划分的呢?一般的解答是根据宽窄依赖划分.那么我们深入源码看看吧 一个action 例如count,会在多次runJob中传递,最终会到一个 ...
- spark 划分stage Wide vs Narrow Dependencies 窄依赖 宽依赖 解析 作业 job stage 阶段 RDD有向无环图拆分 任务 Task 网络传输和计算开销 任务集 taskset
每个job被划分为多个stage.划分stage的一个主要依据是当前计算因子的输入是否是确定的,如果是则将其分在同一个stage,从而避免多个stage之间的消息传递开销. http://spark. ...
- spark 中划分stage的思路
窄依赖指父RDD的每一个分区最多被一个子RDD的分区所用,表现为 一个父RDD的分区对应于一个子RDD的分区 两个父RDD的分区对应于一个子RDD 的分区. 宽依赖指子RDD的每个分区都要依赖于父RD ...
- Spark源代码分析之中的一个:Job提交执行总流程概述
Spark是一个基于内存的分布式计算框架.执行在其上的应用程序,依照Action被划分为一个个Job.而Job提交执行的总流程.大致分为两个阶段: 1.Stage划分与提交 (1)Job依照RDD之间 ...
- Spark分析之DAGScheduler
DAGScheduler概述:是一个面向Stage层面的调度器: 主要入参有: dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, ...
- Spark源代码分析之六:Task调度(二)
话说在<Spark源代码分析之五:Task调度(一)>一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法.这种方法针对接收到的ReviveOffe ...
- [Apache Spark源代码阅读]天堂之门——SparkContext解析
略微了解Spark源代码的人应该都知道SparkContext,作为整个Project的程序入口,其重要性不言而喻,很多大牛也在源代码分析的文章中对其做了非常多相关的深入分析和解读.这里,结合自己前段 ...
- 豌豆夹Redis解决方式Codis源代码剖析:Proxy代理
豌豆夹Redis解决方式Codis源代码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描写叙述: Codis is a proxy b ...
随机推荐
- ajax的几个面试题
一.什么是AJAX(请谈一下你对Ajax的认识)AJAX是“Asynchronous JavaScript and XML”的缩写.他是指一种创建交互式网页应用的网页开发技术.Ajax包含下列技术:基 ...
- 机器学习实践:《Python机器学习实践指南》中文PDF+英文PDF+代码
机器学习是近年来渐趋热门的一个领域,同时Python 语言经过一段时间的发展也已逐渐成为主流的编程语言之一.<Python机器学习实践指南>结合了机器学习和Python 语言两个热门的领域 ...
- 控制div固定在页面的某个位置 ,用js感觉很麻烦 CSS更好一些
CSS代码:<style type="text/css"> html,body { width:100%; height:100%; margin:0px; paddi ...
- 【转】flex中的labelFunction(combox和dataGrid)
Flex中,对于显示一个字段,只需要指定对应字段属性给labelField即可,当需要上述所需要的功能的时候就得做个转换了,在Flex的基于List的组件都有一个labelFunction方法能很简单 ...
- 关于Shiro的退出请求是如何关联到登录请求的思考
一.结论 先给出结论,是因为本身是很简单的道理.假设我们没有使用任何认证授权的框架,就简单的使用Cookie和HttpSession,那么用户登录后的每一个请求是如何关联上这个用户的呢?答案很简单,由 ...
- 深入理解javascript之高级定时器
setTimeout()和setInterval()能够用来创建定时器.其主要的用法这里就不再做介绍了.这里主要介绍一下javascript的代码队列. 在javascript中没有不论什么代码是马上 ...
- vim-normal多行操作命令的使用
命令行命令-<:normal>这个命令可以重复上一个操作.他其实就跟.命令的效果查不到.不同的是,他可以把.的效果,作用于你用可视模式下的多行.例如,如果你想在下面的文字里在每一行加一个; ...
- android图像处理系列之四-- 给图片添加边框(上)
图片处理时,有时需要为图片加一些边框,下面介绍一种为图片添加简单边框的方法. 基本思路是:将边框图片裁剪成八张小图片(图片大小最好一致,不然后面处理会很麻烦),分别对应左上角,左边,左下角,下边,右下 ...
- 3.索引与string进行映射实现高效查找
#include <iostream> #include <typeindex>//类型索引 #include <unordered_map>//红黑树 #incl ...
- 关于SQL分页存储过程的分析
建 立一个 Web 应用,分页浏览功能必不可少.这个问题是数据库处理中十分常见的问题.经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用游标)来实现分页.但这种分页方法仅 ...