Spark內部有兩大類操作,Transformation和Action;
Transformation又分窄依賴操作和寬依賴操作,區分這兩種操作的很簡單,RDD之間轉化過程中沒有shuffle的就是窄依賴,有shuffle的就是寬依賴:
- RDD中數據是分partition的,每個partition分布在特定節點,shuffle就是指在RDD在轉化過程中,一個partition中的數據需要被split成多個分片,傳入到下游RDD中的多個partition中去,比如reduceByKey這類的操作,實際生產中,下游partition中的數據往往依賴于上游多個partition的數據,這樣就是會產生一個問題,如果下游某個partition中的數據缺失,需要重新計算上游多個partition的數據,而重新計算的這些上游partition中又會同時含有下游缺失的數據partition和正常的partition,這就會造成計算的冗余;
- 與之相對的是窄依賴計算,上游一個或多個partition只對應下游一個partition,所以下游某個節點故障后,某個partition缺失數據,上游需要計算的所有partition不含有冗余計算,比如map,filter,union等等;
Spark數據集是RDD,如果數據類型是Tuple2,還提供PariRDDFunctions的一些方法(是通過object RDD addToPariFuncitons隱式包含進來的):
- 如果是窄依賴,比如map操作,生成
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF));
- 如果是寬依賴,比如reduceByKey操作,生成
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine);
其中aggregator定義了數據merge的規則,這個merge包括在map端(類似hadoop中的combiner,也就是partition內部數據merge的規則)和reduce端(partition之間數據merge的規則),這個merge可以是多個value組成更大的集合,例如groupByKey,也可以是多個value合并計算出新的value,比如word count作業中的reduceByKey,根據業務邏輯而定; mapSideCombine是一個boolean,表示是否需要在map端進行merge操作,比如reduceByKey是true,groupByKey是false;
首先來看一下MapPartitionRDD:
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)
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
}
}
其中partitioner(默認HashPartitioner)定義如果存在shuffle,不同的key被shuffle到下游的那一個分片(partition)中去,對于MapPartitionsRDD,不存在這樣的情況;每個RDD都會維護自己的dependencies,是一個Seq,這里的dependency可能是OneToOneDependency(一對一,例如map),可能是RangeDependency(例如union,兩個RDD合并成一個,但是partition不會發生merge,上游RDD多個partition會變成下游RDD的partition range),也可能是ShuffleDependency;
firstParent這里就是dependencies中的第一個,map操作,上游只有一個RDD,也就是只有一個partition,compute操作很簡單,通過split指定上游partition,對其執行f函數,返回的也是一個Iterator,其中firstParent[T].iterator,如果沒有cache或checkpoint則也是一個compute實現;
上面的結果就是RDD內部層層套RDD,最后計算(compute)的時候,由里到外,不斷的遍歷iterator,完成計算,這里直觀感覺上是遍歷多次,但基于scala內部的實現,減少不必要的遍歷;
下面再看ShuffledRDD:
compute內部通過shuffleManager獲取上游shuffle-write產生的數據,根據split,返回iterator,并不包涵其他的函數計算:
override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context).read().asInstanceOf[Iterator[(K, C)]]
}
下面來看一下調用rdd.collect(Spark的Action操作)之后stage的劃分,最后直接的處理函數是handleJobSubmitted,
finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite);
Stage這里面涉及到兩種ResultStage和ShuffleMapStage,每一個Stage都包含若干parent stages,對于這種一對一的RDD DAG 作業,parent stages這個集合中,保存的都只是上游1個stage,是一個單鏈條;劃分Stage的方法很簡單,就是通過ShuffleDependency來判斷;
stage都確定好之后,將stage轉化為task進行計算,計算的條件就是一個stage的所有parent stges都已經計算完成,stage到task的邏輯是:submitMissingTasks;
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
首先得到需要計算哪些partition,然后根據ShuffleMapStage和ResultStage分別生成ShuffleMapTask和ResultTask,然后提交task:
taskScheduler.submitTasks(new TaskSet(tasks.toArray, [stage.id](http://stage.id), stage.latestInfo.attemptId, jobId, properties))
CoarseGrainedExecutorBackend收到LaunchTask之后:
case LaunchTask(data) => if (executor == null) {
logError("Received LaunchTask command but executor was null")
System.exit(1)
} else {
val taskDesc = ser.deserialize[TaskDescription](data.value)
logInfo("Got assigned task " + taskDesc.taskId)
executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber, taskDesc.name, taskDesc.serializedTask)
}
通過executor執行task。
補充說明一點,整個RDD的DAG圖根據ShuffleDependency劃分出若干stage之后,或者是一個ShuffleMapStage,或者是一個ResultStage,對于ResultStage很簡單,從上游讀取數據,聚合之后就可以返回了,對于ShuffleMapStage,他的目標就是完成Shuffle-Write操作,這個實在ShuffleMapTask中的runTask完成的,而需要讀取的數據由上游的RDD的iterator提供,上游如果是普通的RDD,比如MapPartitionsRDD,直接讀取,內部的compute函數完成兩個事情,第一提供iterator,第二完成上游RDD的計算邏輯,即用函數對iterator操作,而這個提供數據iterator又回繼續遞歸調用更上游的RDD的compute,但是如果是ShuffledRDD,則不會遞歸到更上游,而是去reader,讀取上游數據,返回iterator,僅此而已,reader的實現中是到driver拿到mapstatuses,里面包括block的executor位置信息,然后連接executor進行讀取。