一、spark執行過程的一個例子
// rdd_people: id,年齡
var rdd_people = sc.range(1, 100, 1).map(i=>(i, 20+i%80) )
//rdd_score: id,成績
var rdd_score =sc.range(1, 100, 1).map(i=>(i ,i+2))
//兩個進行join
var rdd_res = rdd_people.join(rdd_score)
rdd_res.count()
上面的例子就是一個兩個數據集進行join然后count的一個操作。
那么在運行這段代碼的時候spark內部是如何來處理數據并得到最終得結果的呢。
1.1 spark的角度看你的代碼
當你執行下面的代碼你會看到一些列連接起來的rdd。那么你上面的那些沒有action操作的代碼意義就在于組建一個rdd串起來的一個有向無環圖(DAG)。
rdd_res.toDebugString
你會得到下面得結果:
(2) MapPartitionsRDD[23] at join at <console>:28 []
| MapPartitionsRDD[22] at join at <console>:28 []
| CoGroupedRDD[21] at join at <console>:28 []
+-(2) MapPartitionsRDD[14] at map at <console>:24 []
| | MapPartitionsRDD[13] at range at <console>:24 []
| | ParallelCollectionRDD[12] at range at <console>:24 []
+-(2) MapPartitionsRDD[17] at map at <console>:24 []
| MapPartitionsRDD[16] at range at <console>:24 []
| ParallelCollectionRDD[15] at range at <console>:24 []
1.2 rdd如何得到結果
上面說到我們寫的代碼都會在spark內部轉化成各種rdd的相互連接的dag。那當我們執行count這樣的action操作時,spark如何為我們計算并返回結果的呢。
我們在執行count之后可以在spark ui上看到下圖。
原來spark把這個dag拆分成了幾個stage(也就是任務task的集合),再點擊某個stage就能看到這個stage下都是那些rdd的操作。
1.3 小結
當你在使用rdd這樣的編程范式來表達對數據的處理邏輯時,spark內部就轉化成了各種rdd之間的連接關系;使用spark-sql/dataframe也是這樣,只是上層的表達方式不同,底層都是各種rdd的連接。最后當你執行count之類的action操作,spark就將這一系列的rdd的連接進行分析,生成一些列的task分發到各個executor上去執行具體的操作,然后收集各個executor的結果最終返回。
二、任務生成流程
2.1 action操作
所謂的action操作其實內部都調用了一個函數sc.runJob 這個函數。sc.runJob進行一些函數閉包的處理還有進度條的控制。而sc又會調用DAGScheduler;DAGScheduler把job提交到一個消息隊列中,然后回調handler,handler經過一系列的處理生成task提交到TaskScheduler,由TaskScheduler去把任務分發到各個Executor上運行。
2.2 DAGScheduler 都干了啥
總的來說就是切分stage,建立Task,提交Task到taskScheduler。
2.2.1 stage
stage 分兩種顧名思義,ResultStage就是最后返回結果的那種stage,shuffleMapStage就是中間的Stage,stage是根據shuffle邊界(寬依賴)來劃分的,stage之間自然就是shuffle。(關于stage劃分之后的文章會有)
源碼里會遞歸的訪問rdd發現依賴是ShuffleDependency就會進入下一個stage。
2.2.2 Task
task也一樣分兩種,意思和stage的對應。ResultStage產生的就是ResultTask。
ShuffleTask就負責將rdd的數據計算后使用shuffleWriter把結果寫如磁盤。源碼片段:
#ShuffleMapTask.scala
var writer: ShuffleWriter[Any, Any] = null
try {
val manager = SparkEnv.get.shuffleManager
writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
//這行是關鍵 rdd.iterator就會調用rdd定義好的計算邏輯產生數據,然后writer進行write。
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
writer.stop(success = true).get
}
2.2.3 task的運行
從上面的源碼我們可以看到rdd.iterator。是不是很驚奇,task是運行在遠程機器的executor上的 ,在這里也有rdd的對象,說明rdd是個全局的概念,也是計算邏輯的表示,scheduler計算了rdd 每個partition的位置然后把相應的partition 的task盡量分配到距離近的機器上。然后通過
rdd.iterator調用數據的處理邏輯。
三、count的例子
這里我們以count這個action操作來進行分析rdd是如何得到結果的。
// RDD.scala
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
//utils.scala
def getIteratorSize[T](iterator: Iterator[T]): Long = {
var count = 0L
while (iterator.hasNext) {
count += 1L
iterator.next()
}
count
}
上面的代碼片段可以看到getIteratorSize 這個方法接受一個iterator 然后統計他的長度,iterator 就是每個rdd分區的數據。
sc.runJob返回一個數組,最后在sum 累加起來得到最后的結果。
再來看sc.runJob
// SparkContext.scala
def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {...}
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)
runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res)
results
}
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
resultHandler: (Int, U) => Unit): Unit = {
...
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
...
}
這個runJob重寫了很多次,重要的三個我列在了上面
第二個runJob建立了個results 變量然后調用第三個runJob,這里面就涉及到了兩個函數:
func這個就是需要在executor每個rdd分區上跑的函數也就是上面的Utils.getIteratorSize
resultHandler 也就是這個lambda函數 (index, res) => results(index) = res 給results賦值。
// DAGScheduler.scala
private[scheduler] def handleJobSubmitted(...){
...
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
}
...
val taskBinaryBytes: Array[Byte] = stage match {
case stage: ShuffleMapStage =>
JavaUtils.bufferToArray(
closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))
case stage: ResultStage =>
JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))
}
taskBinary = sc.broadcast(taskBinaryBytes)
DAGScheduler里面ResultStage持有了func這個變量
然后根據stage把task序列化成字節流broadcast出去
// ResultTask.scala
override def runTask(context: TaskContext): U = {
...
val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
func(context, rdd.iterator(partition, context))
}
在task段反序列化task拿到func 執行rdd產出的數據
四、總結
我們回看一下rdd的執行流程,我們使用spark的api構建rdd之間的關系,最后在action操作的時候,dagScheduler利用依賴關系劃分stage,建立任務集,提交Task到TaskScheduler到executor中執行并返回結果。
task在本地被序列化廣播出去,在remoute機器上接受傳來的分區數據進行計算(rdd.iterator(partition, context)),如果是shffletask 就會按分區寫入磁盤,如果是result就運行完返回結果到client。
加我信微 Zeal-Zeng 費免拉你進 知識星球、大數據社群、眾公號(曾二爺) 和優秀的人一起學習