前提回顧
之前分析了Spark Streaming關於DAG的生成,對於Spark Streaming而言,先是通過自己代碼中的各種transform方法來構造各個DStream
之間的關聯關係,然後再通過最後調用action操作的算子處進行回溯,action
算子操作的DStream
作爲outputStream
存儲到DStreamGraph
中的數組中,回溯過程中會找到數據來源處的DStream
,這個DStream
作爲元素存儲到DStreamGraph
中的inputStream
數組中。通過以上兩個inputStream
和outputStream
數組成功存儲了源頭和結尾的DStream
,又依據之前構造的各個DStream
之間的關聯,從而成功的構造完畢了DAG
關係圖。
回顧完Spark Streaming中DAG的生成流程,理論來說RDD
的DAG
生成邏輯應該和DStream
是類似的,我們直接深入代碼來驗證我們的想法。
transform算子分析
顯然和DStream
類似,RDD
中肯定也是通過各種transform
操作來提前構造好各個RDD
之間關係的,爲了驗證這個想法,需要查看各個算子的代碼,先看用的最多的map
算子代碼如下:
> rdd = rdd0.map(...)
--> RDD.scala
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
--> MapPartitionsRDD.scala
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
var prev: RDD[T],
f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator)
preservesPartitioning: Boolean = false,
isOrderSensitive: Boolean = false)
extends RDD[U](prev) {
override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
override def getPartitions: Array[Partition] = firstParent[T].partitions
override def compute(split: Partition, context: TaskContext): Iterator[U] =
f(context, split.index, firstParent[T].iterator(split, context))
override def clearDependencies() {
super.clearDependencies()
prev = null
}
override protected def getOutputDeterministicLevel = {
if (isOrderSensitive && prev.outputDeterministicLevel == DeterministicLevel.UNORDERED) {
DeterministicLevel.INDETERMINATE
} else {
super.getOutputDeterministicLevel
}
}
}
以上代碼分爲三部分,實例用法、底層抽象類RDD.scala
對map
方法的實現以及map
返回類MapPartitionsRDD
的實現。可以很明顯的看到在調用map
方法後,生成新的RDD
的時候會傳入之前的rdd
用於構造新的rdd
,也就是說和dstream
完全類似就是通過transform
來關聯不同rdd
之間的關係。這樣就可以很好的理解流程就是通過各種transform
來關聯rdd
,然後再通過action
來觸發其它操作,這裏說的其它操作應該包括有依據構造的rdd
關係來構造dag
、生成job
、生成stage
等等。
action算子分析
爲了驗證我們的想法,那麼我們就繼續深入代碼,我們挑選collect
算子來作爲入口,來通過collect
算子入口看action
算子內部主要做了哪些事情。
def collect(): Array[T] = withScope {
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
如上爲collect
的實現,可以看到主要就是調用了SparkContext
的runJob
方法,那麼很顯然了,這裏就是深入源碼內部的入口。我們來繼續看runJob
方法的實現:
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
resultHandler: (Int, U) => Unit): Unit = {
if (stopped.get()) {
throw new IllegalStateException("SparkContext has been shutdown")
}
val callSite = getCallSite
val cleanedFunc = clean(func)
logInfo("Starting job: " + callSite.shortForm)
if (conf.getBoolean("spark.logLineage", false)) {
logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
}
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
rdd.doCheckpoint()
}
以上代碼省略了各種重載方法的調用鏈直接到最後的實現方法處,這裏實現也很簡單,主要就是去調用dagScheduler
的runJob
方法,我們來繼續查看dagScheduler
中runJob
的實現:
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
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))
// SPARK-8644: Include user stack trace in exceptions coming from DAGScheduler.
val callerStackTrace = Thread.currentThread().getStackTrace.tail
exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
throw exception
}
}
這裏主要也是爲了調用submitJob
,代碼如下:
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] = {
val jobId = nextJobId.getAndIncrement()
if (partitions.size == 0) {
// 任務分區數爲0則直接返回。
return new JobWaiter[U](this, jobId, 0, resultHandler)
}
val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
// job任務監聽器,任務完成後的數據交給resultHandle來處理
val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
// 發佈消息
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, callSite, waiter,
SerializationUtils.clone(properties)))
waiter
}
如上submitJob
主要就是創建一個job監聽器,然後和任務一起發佈到eventProcessLoop
中,然後會有對應的事件響應處理,對於任務提交JobSubmitted
類型事件的處理方法爲dagScheduler.handleJobSubmitted
方法,具體實現如下:
private[scheduler] def handleJobSubmitted(jobId: Int,
finalRDD: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
callSite: CallSite,
listener: JobListener,
properties: Properties) {
var finalStage: ResultStage = null
try {
// New stage creation may throw an exception if, for example, jobs are run on a
// HadoopRDD whose underlying HDFS files have been deleted.
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
}
// 獲取這個stage觸發的job
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(jobId) = job
activeJobs += job
finalStage.setActiveJob(job)
val stageIds = jobIdToStageIds(jobId).toArray
val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
listenerBus.post(
SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
submitStage(finalStage)
}
這裏可以發現此處會有一個變量叫finalStage
,這個可以理解就是最後一個stage
,我們再來思考我們是如何一路走到這裏的,就是從action
算子觸發一路走到這裏,所以很顯然可以知道這個finalStage
就是DAG
中進行stage
劃分時最後的一個stage
。因爲action
算子就已經是觸發計算任務的地方,確實就應該是一個DAG
的末尾了。
以上分析,可以看出這裏應該也是和dstream
類似,是通過rdd
的action
算子來進行回溯的方式找到源頭rdd
,中間會觸發一些事件暫時用於他處這裏不進行分析,主要圍繞stage
的劃分和啓動流程,那麼對應的就直接看到最後的submitStage
方法,代碼如下:
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) { // 之前已經定義過了jobId
logDebug("submitStage(" + stage + ")")
// 本次提交的stage狀態不是等待、運行、失敗狀態,說明應該是還未正式提交啓動運行的
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
// 獲取當前stage所依賴的且一樣沒有提交的stage,這個過程就是一個回溯的過程
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: " + missing)
if (missing.isEmpty) {
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
submitMissingTasks(stage, jobId.get)
} else {
for (parent <- missing) {
submitStage(parent)
}
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}
到上面這一步的時候,注意最開始調用submitStage
這個方法的時候,傳過來的stage
是由action
觸發得到的,是finalStage
。這裏需要知道的一點就是,每個stage
剛開始創建出來的時候都只是對應一個rdd
,然後在進行回溯的時候,其實是從最開始創建好的DAG
圖譜中最後的finalStage
只是一個調用action
操作的rdd
,通過這個rdd
來回溯它所依賴的父類rdd
,然後判斷和所依賴的rdd
的依賴關係是寬依賴還是窄依賴,如果是窄依賴(即分區之間一對一的依賴)關係,則把依賴的rdd
直接吸收到本次的stage
中;反之如果是寬依賴,則會依據所依賴的rdd
生成一個新的stage
,然後這兩個stage
之間建立聯繫。如此往復一直沿着之前構建的rdd
關係鏈尋找所依賴的rdd
和被依賴rdd
之間的關係來創建stage
,最終會把所有的rdd
劃分爲若干個stage
。
以上介紹的是
stage
劃分的邏輯,還可以補充的一點是關於task
以及job
的產生,對於job
而言,對應的就是一個action
算子,每一個action
操作都會觸發任務執行得到結果,對應會有一個小的DAG
關係圖,這個action
所觸發計算所涉及的計算任務就是一個job
。而具體到劃分完stage
之後,每個stage
所關聯的rdd
中會有若干個partition
,每個partition
計算都會產生一個task
,以上所有的stage
、job
最終都是爲了最終的計算,而最終的計算單位就是以一個一個的partition
作爲單位得到一個交給Executor
執行的task
。