摘要
spark
中, Job
的提交可以分为四个阶段
RDD
生成相互间的依赖关系. 实际上依赖关系是RDD
的固有属性, 也就是说, 每个RDD
生成时, 依赖就已经被生成了DVG
过程, 将Action
算子对应的RDD
作为finalStage
, 由后往前递归的遍历所有RDD
, 并根据宽窄依赖划分Stage
- 根据
partition
数量, 生成对应数量的Task
Worker
调用executor
执行运算逻辑
此次整理了job
提交相关的源码, 源码中完整体现了四个阶段的运行逻辑
runjob
系列方法
spark
是惰性计算的, Action
算子提交时, 会生成Job
, 并真正触发之前的Transformation
算子
源码解析以foreach
方法生成Job
的流程为例
/***** 查看foreach方法 => RDD.scala, 916行 *****/
def foreach(f: T => Unit): Unit = withScope {
// clean方法用于将方法清理为闭包.
// 因为方法中可能包含全局变量, 或者调用了其它类的方法
// 为了分发函数, 需要将相关的类序列化. clean在spark中是很常使用的方法
val cleanF = sc.clean(f)
// this就是调用foreach方法的rdd对象
// rdd对象调用方法, 方法的内部实现是sc提交本对象和相关参数. 这样的转换在spark中也十分常见
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
/***** 查看runJob方法 => SparkContext.scala, 2086行 *****/
// 跳转到了sc. Job相关的任务, 实际执行都要依靠sc
// 继续调用runJob方法. 生成长度等于分区数的数组, 并作为参数传入
def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
runJob(rdd, func, 0 until rdd.partitions.length)
}
/***** 查看runJob方法 => SparkContext.scala, 2057行 *****/
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: Iterator[T] => U,
partitions: Seq[Int]): Array[U] = {
// 其实此次示例的流程中, func已经被clean过了.
val cleanedFunc = clean(func)
// func的参数中, 多了TaskContext, 方便之后的分发
runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
}
/***** 查看runJob方法 => SparkContext.scala, 2038行 *****/
// 在RDD的给定分区上运行函数, 并将结果作为数组返回
// 如果函数针对不同分区有不同的逻辑, 就需要在TaskContext中设定
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int]): Array[U] = {
val results = new Array[U](partitions.size)
//### 通过一个匿名函数, 将数组results传了出去, 从而实现了数组的初始化 ###//
runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res)
results
}
/***** 查看runJob方法 => SparkContext.scala, 2008行 *****/
// Spark所有行为的主要入口
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
// 这个函数内部有一个数组, 该函数运行的结果就是数组完成了初始化, 所以返回的是Unit
resultHandler: (Int, U) => Unit): Unit = {
// 如果任务已经停止, 就抛出异常
if (stopped.get()) {
throw new IllegalStateException("SparkContext has been shutdown")
}
// getCallSite方法会返回当前用户的callSite, callSite记录着请求发起的位置, 也用于记录日志
val callSite = getCallSite
// 日常clean
val cleanedFunc = clean(func)
// 记录日志
logInfo("Starting job: " + callSite.shortForm)
if (conf.getBoolean("spark.logLineage", false)) {
logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
}
//### 该方法的核心是以下代码 ###//
// dagScheduler就是DAG调度器, 我们继续追runJob方法
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
// 将所有stage标记为已结束, 清空UI界面的进度条
progressBar.foreach(_.finishAll())
// 进行CheckPoint
rdd.doCheckpoint()
}
/***** 查看runJob方法 => DAGScheduler.scala, 612行 *****/
// 在给定的RDD上运行Actor算子,并在结果到达时将所有结果传递给resultHandler函数。
// (我们知道resultHandler函数中实际上有一个数组, 等待接收结果集)
def runJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): Unit = {
// 记录开始时间
val start = System.nanoTime
//##### 核心方法, 提交Job到任务队列(后面的代码用于记录日志, 跳转即可) =>
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
// 等待结果, 并记录到日志
ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
waiter.completionFuture.value.get match {
case scala.util.Success(_) =>
logInfo("Job %d finished: %s, took %f s".format
(waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
case scala.util.Failure(exception) =>
logInfo("Job %d failed: %s, took %f s".format
(waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
// 日志中会包括堆栈跟踪.
val callerStackTrace = Thread.currentThread().getStackTrace.tail
exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
throw exception
}
}
job
提交(概览)
/***** 查看submitJob => DAGScheduler.scala, 568行 *****/
// 规范化各参数, 准备提交任务
def submitJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): JobWaiter[U] = {
// partitions是用于记录分区角标的数组, 下述代码检查了partitions是否合法
// 此次代码流程中, partitions的创建逻辑是`0 until rdd.partitions.length`
val maxPartitions = rdd.partitions.length
partitions.find(p => p >= maxPartitions || p < 0).foreach { p =>
throw new IllegalArgumentException(
"Attempting to access a non-existent partition: " + p + ". " +
"Total number of partitions: " + maxPartitions)
}
// 生成jobId
val jobId = nextJobId.getAndIncrement()
if (partitions.size == 0) {
// 如果分区数为0, 以为着没有运算进行, 所以立刻返回
return new JobWaiter[U](this, jobId, 0, resultHandler)
}
// 断言, 分区数必须大于0
assert(partitions.size > 0)
// 强制类型转换, 清除泛型以匹配post方法的参数
val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
// JobWaiter类通过内部通讯机制, 提交运算请求, 并等待接收运算结果
//### partitions.size对应的参数是totalTasks, 也就是分区数决定Task数 ###//
val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
// JobSubmitted用于描述将要运行的job(包含所有Job信息)
// eventProcessLoop内部开启了一个循环进行的线程
// 该线程的run方法会不断读取任务队列中的event(事件), 并调用onReceive方法处理该event.
// post方法就是用于将event事件放入了任务队列, 跳转=>
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, callSite, waiter,
SerializationUtils.clone(properties)))
waiter
}
job
提交(细节)
代码到现在, waiter
, 也就是Listener
, 已经开启并在等待结果
所以job
才能真正被提交, 并标记为active
这个过程的核心就是eventProcessLoop
对象
/***** 查看eventProcessLoop => DAGScheduler.scala, 200行 *****/
// DAGScheduler类生成对象的时候, eventProcessLoop就被new出来了
private[scheduler] val eventProcessLoop = new DAGSchedulerEventProcessLoop(this)
/***** 查看DAGSchedulerEventProcessLoop => DAGScheduler.scala, 1658行 *****/
private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)
// post方法是继承自EventLoop类, 线程的具体实现也在父类
// 简而言之, 调用post方法就会延迟调用onReceive方法, 我们关注后者即可
extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging {
//### DAG调度程序的主事件循环 ###//
override def onReceive(event: DAGSchedulerEvent): Unit = {
val timerContext = timer.time()
try {
// 调用了doOnReceive方法
doOnReceive(event)
} finally {
timerContext.stop()
}
}
private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
// 模式匹配, 匹配的到JobSubmitted
case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
// 终于真正处理Job了. 跳转=>
dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
//### 这里省略了其余case代码, 还省略了该类中非关键的方法和属性 ###//
}
}
/***** handleJobSubmitted方法 => DAGScheduler.scala, 839行 *****/
private[scheduler] def handleJobSubmitted(jobId: Int,
// 这个rdd就是Action算子对应的rdd
finalRDD: RDD[_],
// 运算逻辑, 借助TaskContext可以传递更为复杂的逻辑
func: (TaskContext, Iterator[_]) => _,
// 分区
partitions: Array[Int],
// 发起请求的位置
callSite: CallSite,
// 就是JobWaiter, 等待接收运算结果
listener: JobListener,
// 配置文件
properties: Properties) {
var finalStage: ResultStage = null
try {
// 生成新stage时可能会抛出异常, 比如RDD对应的hdfs上的文件被删除了
// 这段代码, 将Actor算子对应的RDD封装为finalStage
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
} catch {
case e: Exception =>
logWarning("Creating new stage failed due to exception - job: " + jobId, e)
listener.jobFailed(e)
return
}
// 终于真正生成了ActiveJob
val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
// 清空缓存, 记录日志
clearCacheLocs()
logInfo("Got job %s (%s) with %d output partitions".format(
job.jobId, callSite.shortForm, partitions.length))
logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
logInfo("Parents of final stage: " + finalStage.parents)
logInfo("Missing parents: " + getMissingParentStages(finalStage))
val jobSubmissionTime = clock.getTimeMillis()
// jobIdToActiveJob是个HashMap, 这句代码将id存储为active键, 并记录对应的job
jobIdToActiveJob(jobId) = job
// 将job记录为active
activeJobs += job
finalStage.setActiveJob(job)
// 完善下stage相关信息
val stageIds = jobIdToStageIds(jobId).toArray
val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
// 提前开启Listener, 等待接收结果
listenerBus.post(
SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
//##### 核心方法, 跳转 =>
submitStage(finalStage)
}
Stage
生成
通过handleJobSubmitted
和getMissingParentStages
两个方法, 来将此次job
分割为更细粒度的stage
, 也就是DAG
过程的核心
handleJobSubmitted
和getMissingParentStages
两个方法都有递归, getMissingParentStages
的递归会在回溯到宽依赖时停止, handleJobSubmitted
从宽依赖处切分job
, 从而生成stage
, 然后继续递归回溯, 从而完成对整个job
的处理
简而言之, stage
的划分是从action
算子对应的rdd
开始的, 然后通过递归, 不断回溯, 直至完成job中rdd的遍历处理
/***** handleJobSubmitted方法 => DAGScheduler.scala, 921行 *****/
// 该方法用于划分stage, 参数中传入的是finalStage
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
// if确认jobId存在
if (jobId.isDefined) {
// 先把finalStage记录到日志上
logDebug("submitStage(" + stage + ")")
// waitingStages等方法实际上是去对应的HashSet上查看stage是否存在, 以此来确认stage是未经处理的
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
// getMissingParentStages方法生成Stage集合. 跳转 =>
// <= 对宽依赖的父rdd的依赖分析, 由此处的递归来实现
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: " + missing)
// 没有宽依赖时, 终止递归
if (missing.isEmpty) {
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
//### 作为递归的终止条件, 该方法实际上会提交所有stage ###//
submitMissingTasks(stage, jobId.get)
} else {
for (parent <- missing) {
// 递归
submitStage(parent)
}
// 将stage标记为等待(放入waitingStages集合, 对应上面的if条件判断语句)
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}
// 至此, stage划分完成, 且全部进入waitingStages等待运行
// 运行的结果会被Listener收集
/***** getMissingParentStages方法 => DAGScheduler.scala, 442行 *****/
private def getMissingParentStages(stage: Stage): List[Stage] = {
val missing = new HashSet[Stage]
val visited = new HashSet[RDD[_]]
// 使用Stack, 以避免递归可能导致的栈溢出
val waitingForVisit = new Stack[RDD[_]]
def visit(rdd: RDD[_]) {
// !visited(rdd)就是visited集合中不存在rdd
if (!visited(rdd)) {
// 如果不存在, 就像先添加进来
visited += rdd
// 查看rdd的高速缓存集合是否为空
val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
// 如果没缓存
if (rddHasUncachedPartitions) {
// 遍历该rdd的依赖
for (dep <- rdd.dependencies) {
dep match {
// 如果是宽依赖(可能发生shuffle的依赖)
case shufDep: ShuffleDependency[_, _, _] =>
// 生成Stage
val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
if (!mapStage.isAvailable) {
// 将生成的stage存入missing集合
missing += mapStage
}
// 如果是窄依赖
case narrowDep: NarrowDependency[_] =>
// 等会儿追溯(递归)
// 注意到rdd和其父rdd, 如果是窄依赖, 就会递归的找父rdd的父rdd
// 但是如果是宽依赖, 就不递归了
// 对宽依赖的父rdd当然也需要前溯, 这个递归是由上一个方法(handleJobSubmitted)来完成的
waitingForVisit.push(narrowDep.rdd)
}
}
}
}
}
waitingForVisit.push(stage.rdd)
// 如果还有RDD没被访问过的, 就递归
while (waitingForVisit.nonEmpty) {
visit(waitingForVisit.pop())
}
// 返回MissStage的集合, 我们也返回去继续看代码 =>
missing.toList
}
Task
生成
在这个方法中, 每个Stage
生成了相应数量的Task
至此, job
流程中主要的代码逻辑列举完毕
/***** submitMissingTasks方法 => DAGScheduler.scala, 944行 *****/
private def submitMissingTasks(stage: Stage, jobId: Int) {
// 将要操作的stage记录到日志
logDebug("submitMissingTasks(" + stage + ")")
// 计算出要计算的分区id的索引
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
// 加载配置文件
val properties = jobIdToActiveJob(jobId).properties
// 将stage标记为running
runningStages += stage
// case语句判断stage类型
stage match {
// 先判断ShuffleMapStage, 如果不是再进行后续判断
case s: ShuffleMapStage =>
outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)
// 实际上只有finalStage是ResultStage
case s: ResultStage =>
outputCommitCoordinator.stageStart(
stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)
}
// 中间代码省略, 跳转 => 1021行
val tasks: Seq[Task[_]] = try {
val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
stage match {
case stage: ShuffleMapStage =>
stage.pendingPartitions.clear()
partitionsToCompute.map { id =>
val locs = taskIdToLocations(id)
// part就是task的数量, 由rdd的分区数决定
val part = stage.rdd.partitions(id)
stage.pendingPartitions += id
// 根据最佳位置算法, 生成Task
// 最佳位置算法其实说白了,就是从stage的最后一个RDD开始,
// 由后向前, 寻找已完成缓存的分区(cache或checkpoint).
// task最佳位置, 就是已完成缓存的分区位置.
// 因为这样的话, task就可以在那个节点上执行, 而不需要计算之前的RDD了。
new ShuffleMapTask(stage.id, stage.latestInfo.attemptId,
taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
Option(sc.applicationId), sc.applicationAttemptId)
}
case stage: ResultStage =>
partitionsToCompute.map { id =>
val p: Int = stage.partitions(id)
// 两个case都是相同的part逻辑
val part = stage.rdd.partitions(p)
val locs = taskIdToLocations(id)
// 和上面的case类似, 找最佳位置
new ResultTask(stage.id, stage.latestInfo.attemptId,
taskBinary, part, locs, id, properties, serializedTaskMetrics,
Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
}
}
} catch {
case NonFatal(e) =>
abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
runningStages -= stage
return
}
// 之后还有一段健壮性判断, 也省略不写
}