在該系列的上一篇文章中,較爲詳細的描述了Spark程序的生命週期,這一篇我們以一段Spark代碼爲例,來詳細拆解一下Spark程序的執行過程。
一、示例代碼:
val ss = SparkSession.builder().appName("localhost").master("local[*]").getOrCreate() val df1 = ss.range(2, 10, 2).toDF() val df2 = ss.range(0, 20, 4).toDF() val df11 = df1.repartition(3) val df21 = df2.repartition(4) val df12 = df11.selectExpr("id * 2 as id") // select1 val df3 = df21.join(df12, "id") val df4 = df3.selectExpr("sum(id)") // select2 df4.collect().foreach(println(_)) df4.explain()
二、打印的執行計劃和DAG圖
== Physical Plan == AdaptiveSparkPlan isFinalPlan=true +- == Final Plan == *(5) HashAggregate(keys=[], functions=[sum(id#3L)]) +- ShuffleQueryStage 3 +- Exchange SinglePartition, ENSURE_REQUIREMENTS, [id=#175] +- *(4) HashAggregate(keys=[], functions=[partial_sum(id#3L)]) +- *(4) Project [id#3L] +- *(4) BroadcastHashJoin [id#3L], [id#6L], Inner, BuildRight, false :- ShuffleQueryStage 0 : +- Exchange RoundRobinPartitioning(4), REPARTITION_BY_NUM, [id=#57] : +- *(1) Range (0, 20, step=4, splits=8) +- BroadcastQueryStage 2 +- BroadcastExchange HashedRelationBroadcastMode(List(input[0, bigint, false]),false), [id=#134] +- *(3) Project [(id#0L * 2) AS id#6L] +- ShuffleQueryStage 1 +- Exchange RoundRobinPartitioning(3), REPARTITION_BY_NUM, [id=#62] +- *(2) Range (2, 10, step=2, splits=8)
DAG圖:
三、分析
1、首先看兩個toDF方法和對應兩個DataFrame的repartition方法
默認用range方式創建DataFrame時的分區數是8個,而我們repartition的分區數分別爲3和4,分區數不同觸發shuffle。
出現shuffle是Spark中stage劃分的原則,所以此處兩個repartition觸發了兩次shuffle。
這兩次shuffle對應命令行中即下面兩處:
對應DAG圖中是如下所示:
2、第一個selectExpr
該行代碼對應執行計劃中是【(3) Project】那一行,在DAG圖中爲下面標識處,因爲只相當於一個map,故無需移動數據
3、join操作
代碼示例中的join操作,是進行了一次內關聯,正常來說此處是需要觸發shuffle的,即兩個df均需進行數據的移動。但看DAG圖會發現只有右邊的DF即df12發生了Exchange即shuffle,爲什麼會這樣呢?
這時Spark針對join的一種優化。Spark認爲,如果參與join的雙方,有一方的數據量少於10M,則會將該DF轉成廣播變量發給每個節點,每個節點中的另一個DF中的數據就可直接在本節點做map操作,減少數據的轉移量。
當然,如果兩個DF都少於10M,則取數據量較少的一方進行廣播。本示例中對df12進行的廣播,即BroadcastExchange。
廣播完之後就是BroadcastHashJoin了,在執行計劃中進行了形象的關聯,用冒號的連線表示兩個DF的關聯。
4、第二個selectExpr的sum操作
在DAG圖中可以看到,sum的操作涉及兩次HashAggregate和一次Shuffle(Exchange):
而通過執行計劃可以得到更詳細的信息,如下圖所示。首先的第一個HashAggregate是partial_sum部分求和,即先對每個分區內的id求和。
然後是一個SinglePartition的shuffle,它的作用就是把所有分區的數據合併到一個分區上去,最後一個HashAggregate即對該單個分區的所有數據進行求和。
尾聲
至此,這個簡單的spark用例的執行流程便分析完了,但其中還有很多隱藏的知識點。
比如DAG圖中的WholeStageCodegen是什麼? 其實它是Spark對迭代計算的一種優化,它可以將每行數據進行計算時所用的小函數內聯成一兩個大函數執行,這樣可以充分利用編譯器以及CPU的優化特性,將執行性能提升一個數量級。細心的話會發現在日誌的執行計劃中,有的行前面會有一個星號*標識,該標識就表示該段代碼啓用了codegen的代碼生成,是優化過的。
比如Spark還有什麼其他的內置優化點?爲什麼這裏用的是SingePartition而不是其他Partition方式?RoundRobinPartitioning的作用原理是什麼?Spark性能優化如何分析進行...
問題太多,勿急,且慢慢研究。