在Spark中有幾個重要概念:
- Application - 源代碼就是應用
- Job - action會觸發一個job
- Stage - 按照寬窄依賴來分的
- Task - 最終執行的工作
- Driver - 跑源代碼main func,跑各種并行操作的機器
- Executor - 執行task細節的機器
我們從以下簡單的一行代碼入手,來看spark中的各個術語的含義。
scala>sc.textFile("README.md").filter(_.contains("Spark")).count
其中,sc代表SparkContext,它通過一些default的SparkConfig構建出來。這一行代碼就算是一個Application。sc通過textFile, filter操作RDDs,最后count這個Action觸發一個job。
整體上描述一下spark的運行:Application會運行在driver上,driver會根據代碼中的action創建并提交job(runJob/submitJob)。然后從job的最后一個RDD朝前演算,遇到一個寬依賴就創建一個stage。最后以stage為單位創建task集合,并在excutor中執行每項task
spark拆分任務的流程圖如下:
涉及到的幾個class:
SparkContext, DAGScheduler, DAGSchedulerEventProcessLoop, TaskScheduler
幾個class的相互關系
- SparkContext中初始化DAGScheduler, TaskScheduler
- DAGScheduler中初始化DAGSchedulerEventProcessLoop(eventProcessLoop)
- DAGScheduler的構造函數參數中包含TaskScheduler
流程介紹
整個過程就是將RDD DAG按照寬窄依賴切分成Stage DAG:
- 首先在SparkContext初始化的時候會創建DAGScheduler,這個DAGScheduler每個應用只有一個。然后DAGScheduler創建的時候,會初始化一個事件捕獲對象DAGSchedulerEventProcessLoop,并且開啟監聽(start)。之后我們的任務都會發給這個事件監聽器,它會按照任務的類型創建不同的任務(doOnReceive)。
- 再從客戶端程序方面說,當我們調用action操作的時候,就會觸發runJob,它內部其實就是向前面的那個事件監聽器提交一個任務。
- 然后事件監聽器調用DAGScheduler的handleJobSubmitted做真正的處理
- 處理的時候,要去創建一個ResultStage(每個job只有一個ResultStage,其余的都是ShuffleMapStage),這會根據RDD的依賴關系,按照廣度優先(總是先找到自己的所有直接parents)的思想遍歷所有RDD,遇到ShuffleRDD就創建一個新的stage,最終形成一個以ResultStage為尾的stage DAG(透過訪問ResultStage,朝前不停遍歷就可以找到所有的stage)
- 形成stage DAG圖后,遍歷等待執行的stage列表,如果這個stage所依賴的父stage執行完了,它就可以執行了;否則還需要繼續等待。
- 最終stage會以taskset的形式,提交給TaskScheduler,最后提交給excutor。
private def getMissingAncestorShuffleDependencies(
rdd: RDD[_]): Stack[ShuffleDependency[_, _, _]] = {
val ancestors = new Stack[ShuffleDependency[_, _, _]]
val visited = new HashSet[RDD[_]]
// We are manually maintaining a stack here to prevent StackOverflowError
// caused by recursively visiting
val waitingForVisit = new Stack[RDD[_]]
waitingForVisit.push(rdd)
while (waitingForVisit.nonEmpty) {
val toVisit = waitingForVisit.pop()
if (!visited(toVisit)) {
visited += toVisit
getShuffleDependencies(toVisit).foreach { shuffleDep =>
if (!shuffleIdToMapStage.contains(shuffleDep.shuffleId)) {
ancestors.push(shuffleDep) //廣度遍歷
waitingForVisit.push(shuffleDep.rdd)
} // Otherwise, the dependency and its ancestors have already been registered.
}
}
}
ancestors
}
參考: http://www.cnblogs.com/xing901022/p/6674966.html
stage拆分的整個函數調用過程如下:
舉例說明:
如下圖,spark job依賴關系:
上圖抽象如下:
[E] <--------------
\
[C] <------[D]------[F]--(s_F)----
\
[A] <-----(s_A)----- [B] <-------- [G]
Note: [] means an RDD, () means a shuffle dependency.
結果解析
對 G 調用 creatResultStage,先為所有parent創建ShuffleMapStage,然后創建本身的 ResultStage。 如上圖getOrCreateParentStages會先創建上游 stage1和stage2, 然后創建自己的 stage3
getOrCreateParentStages 會調用 getShuffleDependencies 獲得 rdd_G 所有直接寬依賴 HashSet(s_F, s_A), 然后遍歷集合,對 s_F 和 s_A 調用 getOrCreateShuffleMapStage
對 s_A 調用 getOrCreateShuffleMapStage, shuffleIdToMapStage 中獲取判斷為None, 對 rdd_A 調用getMissingAncestorShuffleDependencies, 返回為空, 對 s_A 調用 createShuffleMapStage, 由于rdd_A 沒有parent stage 直接就創建 stage1 返回
對 s_F 調用 getOrCreateShuffleMapStage, shuffleIdToMapStage 中獲取判斷為None, 對 rdd_F 調用 getMissingAncestorShuffleDependencies, 返回為空, 對 s_F 調用 createShuffleMapStage, 由于rdd_F 沒有parent stage 直接就創建 stage2 返回
把 List(stage1,stage2) 作為 stage3 的 parents stages 創建 stage3
至此,Stage都建立起來之后,就要開始執行各個stage
submitStage -> getMissingParentStages -> submitMissingTasks -> submitStage
整體上來講:
- 在submitStage中會計算stage之間的依賴關系,依賴關系分為寬依賴和窄依賴兩種
- 如果計算中發現當前的stage沒有任何依賴或者所有的依賴都已經準備完畢,則提交task
- 提交task是調用函數submitMissingTasks來完成
-當前stage執行完畢之后,再調用函數submitStage來執行child stage
TaskScheduler在SparkContext初始化期間就會初始化并且start,其backend會根據deploy mode作相應調整
submitMissingTasks -> taskScheduler(TaskSchedulerImpl).submitTasks -> backend.reviveOffers -> executor.launchTask -> threadPool.execute
private def submitMissingTasks(stage: Stage, jobId: Int) {
......
......
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)
val part = stage.rdd.partitions(id)
stage.pendingPartitions += id
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)
val part = stage.rdd.partitions(p)
val locs = taskIdToLocations(id)
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
}
if (tasks.size > 0) {
logInfo(s"Submitting ${tasks.size} missing tasks from $stage (${stage.rdd}) (first 15 " +
s"tasks are for partitions ${tasks.take(15).map(_.partitionId)})")
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))
} else {
// Because we posted SparkListenerStageSubmitted earlier, we should mark
// the stage as completed here in case there are no tasks to run
markStageAsFinished(stage, None)
val debugString = stage match {
case stage: ShuffleMapStage =>
s"Stage ${stage} is actually done; " +
s"(available: ${stage.isAvailable}," +
s"available outputs: ${stage.numAvailableOutputs}," +
s"partitions: ${stage.numPartitions})"
case stage : ResultStage =>
s"Stage ${stage} is actually done; (partitions: ${stage.numPartitions})"
}
logDebug(debugString)
submitWaitingChildStages(stage)
}
注意:
- stageID從1開始,按照"爺->父->子->孫"一次遞增1
private val nextStageId = new AtomicInteger(0)
val id = nextStageId.getAndIncrement()
- 每個job只有一個ResultStage,其余的都是ShuffleMapStage
- 每個Stage的實例中,都包含一個parents的屬性,這樣就可以透過"孫"stage朝前找到所有的"祖先"stage