深入原始碼理解Spark RDD的資料分割槽原理

GetMyCode發表於2020-08-20

通過記憶體建立RDD的分割槽設定

1、示例程式碼

在建立RDD的時候,我們可以從記憶體中進行建立;輸出儲存為檔案。為了演示效果,我們的示例程式碼如下:

 1 import org.apache.spark.{SparkConf, SparkContext}
 2 
 3 object Spark02RddParallelizeSet {
 4   def main(args: Array[String]): Unit = {
 5     System.setProperty("hadoop.home.dir", "C:\\Hadoop\\")
 6     val spark = new SparkConf().setMaster("local[*]").setAppName("RddParallelizeSet")
 7     val context = new SparkContext(spark)
 8 
 9     val list = List(1, 2, 3, 4)
10 
11     // TODO: 從記憶體建立RDD,並且設定並行執行的任務數量
12     // numSlices: Int = defaultParallelism
13     val memoryRDD = context.makeRDD(list, 1)
14     memoryRDD.saveAsTextFile("output")
15 
16     val memoryRDD1 = context.makeRDD(list, 2)
17     memoryRDD1.saveAsTextFile("output1")
18 
19     val memoryRDD2 = context.makeRDD(list, 3)
20     memoryRDD2.saveAsTextFile("output2")
21 
22     val memoryRDD3 = context.makeRDD(list, 4)
23     memoryRDD3.saveAsTextFile("output3")
24 
25     val memoryRDD4 = context.makeRDD(list, 5)
26     memoryRDD4.saveAsTextFile("output4")
27 
28     // TODO: 結束
29     context.stop()
30   }
31 }

上面的程式碼裡,我們從記憶體中建立了5個RDD,每個RDD設定了不同的分割槽數。

 

2、執行結果

(1)1個分割槽的RDD,效果如下圖所示:

 

在output資料夾中,只包含了一個分割槽檔案 part-00000 ,其檔案內容如下圖所示:

 

(2)2個分割槽的RDD,效果如下圖所示:

 

在output1資料夾中,包含了兩個分割槽檔案 part-00000 以及 part-00001,二者檔案內容如下圖所示:

 

 

(3)3個分割槽的RDD,效果如下圖所示:

 

在output2資料夾中,包含了三個分割槽檔案 part-00000 、part-00001、part-00002,三者檔案內容如下圖所示:

 

 

 

(4)4個分割槽的RDD,效果如下圖所示:

 

在output3資料夾中,包含了四個分割槽檔案 part-00000、part-00001、part-00002、part-00003,四者檔案內容如下圖所示:

 

 

 

(5)5個分割槽的RDD,效果如下圖所示:

 

 

 

 

 在output4資料夾中,包含了五個分割槽檔案 part-00000、part-00001、part-00002、part-00003、part-00004,五者檔案內容如下圖所示:

 

 

 

 

 

 

3、分析結果

仔細看看上面圖片中的結果,不難發現其中肯定深藏貓膩。不同分割槽數的設定,都存在不同的輸出效果。要想深究其中緣由,有必要去了解Spark的在這一塊的原始碼實現。進入到 makeRDD( ) 這個方法中,可以看到如下原始碼:

 

在圖片中用紅色方框框起來的部分,是一個方法,其中傳入了兩個引數,第一個 seq 就是在我們自己寫的程式碼中,自己定義的那個 List;第二個 numSlices 就是我們自己寫的程式碼中的那個分割槽數。

再點進 parallelize( seq, numSlices) 這個方法中去,可以看到如下原始碼:

 

parallelize[T: ClassTag] 這個方法位於 SparkContext.scala 這個檔案中。
在這個方法裡,withScope是用來做DAG視覺化的,{ } 裡面包含了一個 assertNotStopped( ) 方法 以及一個 ParallelCollectionRDD 物件例項。看樣子,應該是 ParallelCollectionRDD 對我們檢視分割槽有幫助,於是點選進入到這個類當中,可以看到如下的原始碼:

 

紅色線框框出的這個名為 getPartitions 的方法,返回的是一個分割槽的陣列,ParallelCollectionRDD.slice( ) 呼叫的是 ParallelCollectionRDD 伴生物件裡面的一個方法,看樣子應該是這個方法裡的程式碼規定了怎麼進行分割槽的劃分了,於是,進到這個方法裡面,果不其然,可以看到如下程式碼塊:

 

 

在 slice[T: ClassTag](seq: Seq[T], numSlices: Int): Seq[Seq[T]] 這個方法中,我們可以看到一個關於 seq 這個傳入的引數的模式匹配:

 

 

在上圖的這一段程式碼中,模式匹配第一個匹配的是 Range 型別,很明顯與我們傳入的 List 型別不一致,因此 case r: Range 這一塊的匹配程式碼可以跳過。第二個匹配的也是Range,相比於第一個匹配的Range 整形型別 而言,第二個則可以匹配更多種型別的 Range。當然第二個case也不是我們要看的。那麼就剩下第三個 "case _" 這種情況了。

在第三個情況中,首先將 我們傳入的 List 轉換為了一個 array,為什麼要有這一步,原始碼中也給出了註釋:To prevent O(n ^ 2) operations for List etc。之後呼叫了positions( ) 方法,將轉換後的陣列的長度以及分割槽數量傳入,該方法原始碼如下:

 

 

我的分析過程如下,資料是(1,2,3,4),分割槽數量是1的情況:

 

 

4、小結

通過上面的分析過程,終於知道樣例資料為什麼會做出令人費解的切分。讀者們可以試試將List資料型別轉換成Range型別,看看不同分割槽下,會有怎麼樣的分割槽結果。

相關文章