Spark的Shuffle(一) - SortShuffleWriter

上一篇“shuffle的一些概念”中提到了三種shuffle的方式,此處先來分析下SortShuffleWriter,結合代碼一起調試下看看它內部到底是如何運行的。

選擇帶有聚合的算子調試就行了,例如對一個pairRDD進行reduceByKey操作,然後就可以跳到對應的源碼裏面了,可以看出reduceByKey算子使用的是確實是SortShuffleWriter:

直接跑到運行Task的代碼中看它到低是如何進行Shuffle的。

Ps:這裏我有個疑問,我代碼裏面並沒有選擇Kryo的序列化方式,爲什麼這裏顯示的是kryo?默認的不應該是JavaSerializer麼?

 

SortShuffleWriter流程如下所示

 

 

Shuffle相關的一些配置項:

這兩個用於控制shuffle時落盤的頻率:

spark.shuffle.file.buffer: 默認32K

spark.shuffle.spill.batchSize: 默認10000

 

shuffle時臨時文件相關配置:

spark.shuffle.spill.initialMemoryThreshold: shuffle時初始的緩衝區大小,擴容時會擴容一輩,申請不到一倍的內存就會生成臨時shuffle文件,默認5*1024*1024(5M)

spark.shuffle.spill.numElementsForceSpillThreshold: shuffle時,緩衝區的數量達到這量就會生成臨時shuffle文件,默認Long.MaxValue

 

Shuffle主要過程個人總結爲如下5步

接下來對上面這些流程進行一步步的詳細分析。

 

1.創建用於對數據進行排序、聚合、寫緩存等操作ExternalSort

比較重要的參數是下面4個:

private val fileBufferSize = conf.getSizeAsKb("spark.shuffle.file.buffer", "32k").toInt * 1024

private val serializerBatchSize = conf.getLong("spark.shuffle.spill.batchSize", 10000)

@volatile private var map = new PartitionedAppendOnlyMap[K, C]

@volatile private var buffer = new PartitionedPairBuffer[K, C]

由於Shuffle將數據寫到磁盤之前,會先寫到緩衝區中,滿了之後纔會溢出寫到磁盤。寫磁盤的時候默認32K或者10000條數據寫一次磁盤。

後面兩個數據結構就是所謂的緩衝區,map適合有Aggregator場景,buffer則不是。

 

2.接着調用ExternalSort的insertAll()方法將數據寫入到緩衝區。如果數據量太多,會flush()多個臨時的shuffle文件

map.changeValue(…)是根據用於定義的函數,更新緩衝區中的值,緩衝區裏面長這樣:

接下來的maySpillCollection()方法內部會調用mayBeSpill()判斷是否需要生成臨時的Shuffle文件(所以幹嘛要分成兩個方法呢?一個方法不就好了麼?!):

Sparkenv.get.conf.getLong("spark.shuffle.spill.initialMemoryThreshold", 5 * 1024 * 1024)

緩衝區初始化大小,可以擴大緩衝區的話,會嘗試申請一倍的內存Sparkenv.get.conf.getLong("spark.shuffle.spill.numElementsForceSpillThreshold", Long.MaxValue)

默認達到Long類型的最大長度纔會刷新緩衝區,我早就設置成10了,哈哈哈,就是爲了看它是如何生成臨時文件的。

 

2.1分析下如何生成臨時Shuffle文件:

mayBeSpill()就是根據上面定義的兩個參數,將內存中的數據刷寫到磁盤上生成臨時的Shuffle文件:

注意:寫臨時文件完成之後會釋放這個Task所佔用的內存空間,到它最初所佔有的內存大小,就是那個默認的5M:

寫臨時文件的方法是spill(collection),它的核心是生成一個inMemoryIterator迭代器,然後使用這個迭代器將數據刷到磁盤上,並記錄下這個臨時shuffle文件。

 

2.1.1獲取內存數據迭代器:

因爲這裏reduceByKeyy使用的是PartitionedAppendOnlyMap結構,所以這裏就看下它的迭代器:

這個迭代器的key是一個tuple(partition ID,K),然後還有個keyComparator,估計是還要按照key排序一把,點進去看下。

首先是生成一個partitionKeyComparator,先比較partition,在比較key

這個comparator用於對AppendOnlyMap中的元素進行排序

最後返回的這個迭代器迭代的時候,會按照partition的順序返回

 

2.1.2將內存中的數據刷寫到磁盤上生成臨時Shuffle文件

spillMemoryIteratorToDisk(inMemoryIterator:WritablePartitionedIterator)通過上面inMemoryIterator的將數據刷寫到磁盤生成臨時文件,臨時文件的目錄和文件名稱命名如下:

 

 

2.1.3可以看到最終會生成很多臨時的temp_shuffle文件

至此,寫內存的過程就結束了。注意如果數據量少的話,就不會有這些臨時的Shuffle文件,而是全部在內存裏面。

 

3.接着將map端緩存的文件寫入到磁盤中,最終只會生成一個Shuffle文件,臨時Shuffle文件會被合併到一起。

writePartitionedFile()方法就是往最終的那一個Shuffle文件中寫數據,內部流程如下所示:

this.partitionedItreator這個迭代器是按照下面的方式獲取的:

最終會爲每個partition生成一個迭代器,並且每個partition中的元素已經完成了agg和order操作(如果定義過這兩個操作的話)。

 

4.生成索引文件

這個方法看了下,好像沒什麼好說的,就是寫了一個IndexFile,這個文件記錄的是每個partition在那個正式的Shuffle文件的偏移量,以便reduce任務拉取時使用。

 

5.返回MapStatus

MapStatus會返回給Driver,這樣Driver就可以知道每個Executor上的Shuffle文件的位置了。

如果MapStatus太大的話會對它進行壓縮。

 

 

 

參考:

《Spark內核設計的藝術》

https://www.cnblogs.com/20125126chen/p/4534190.html(寫文件時,文件系統如何定位)

http://www.importnew.com/14111.html(寫文件時,Java I/O底層是如何工作的)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章