概述
本文分析shuffleWriter的實現類:BypassMergeSortShuffleWriter的詳細實現原理。
BypassMergeSortShuffleWriter介紹
BypassMergeSortShuffleWriter的啓用條件
shuffl過程中進行write時,會根據不同的情況選擇不同的shuffleWriter實現類。從前面的幾篇文章《spark2原理分析—shuffle框架的實現概要分析》《spark2原理分析-shuffleWriter:SortShuffleWriter實現分析》中得知:BypassMergeSortShuffleWriter的啓用條件是:
SortShuffleManager僅在下列情況下選擇此寫入方式:
- 沒有指定排序(Ordering)
- 沒有指定聚合器(Aggregator)
- 分區數少於參數:spark.shuffle.sort.bypassMergeThreshold,默認是:200。
BypassMergeSortShuffleWriter要點說明
- 它會爲每個分區創建一個磁盤寫入的對象,和一個文件寫入流對象
- 它會先爲每個分區數據創建一個臨時文件
- 當數據的分區太大時,創建的輸出流對象和臨時文件將會很多,佔用的資源可能會很多,可能導致任務性能降低。
- 該類實現了基於排序shuffle(sort-based shuffle)的hash模式的shuffle方式。
- 這種寫入方式將傳入的數據寫入單獨的文件,每個reduce分區生成一個文件,再把每個分區文件進行合併,形成單個輸出文件。
- 數據不會緩存在內存中,它以可以通過{@link org.apache.spark.shuffle.IndexShuffleBlockResolver}供給/消費的格式寫入輸出。
- 要注意的是:由於這種方式會同時爲每個分區創建一個磁盤寫入的對象和一個文件寫入流對象,爲每個分區數據創建一個臨時文件,所以當shuffle具有大量reduce分區時,這種寫入方式效率會比較低。
BypassMergeSortShuffleWriter類的實現
BypassMergeSortShuffleWriter對象的構造
- 獲取參數spark.shuffle.file.buffer的值,該參數是shuffle過程寫數據到磁盤文件時的buffer,默認是32k。
- 獲取參數spark.file.transferTo的值,該參數是決定寫磁盤文件的方式,若該參數爲True,則使用NIO的方式來寫入數據,否則使用普通寫入方式。
- 接下來需要獲取shufflehandle中的ShuffleDependency對象,通過該對象得到分區器和分區個數等數據。
- 接下來設置序列化工具對象,和shuffleBlockResolver對象,該對象用來創建和維護shuffle的數據的邏輯塊和物理文件位置之間的映射的對象。
BypassMergeSortShuffleWriter的實現
write函數的實現
函數原型如下:
public void write(Iterator<Product2<K, V>> records)
該函數的實現流程如下:
- 判斷寫入記錄是否已經遍歷到最後一條,若是則:
- 若記錄還沒有遍歷到最後一條,則創建一個序列化工具SerializerInstance類對象:serInstance,該對象用來對數據進行序列化操作。
- 爲每個分區創建一個DiskBlockObjectWriter對象,該對象負責把對象寫入磁盤。
partitionWriters = new DiskBlockObjectWriter[numPartitions];
- 爲每個分區創建一個FileSegment對象,該對象用來記錄每個分區數據的長度,file對象,偏移量offset的值。
partitionWriterSegments = new FileSegment[numPartitions];
- 有了以上的信息,現在就可以開始處理分區數據了。這一步會遍歷所有分區,創建一個blockId和File文件對象的對應關係的元組。通過這兩個對象來創建DiskBlockObjectWriter對象,從而爲數組partitionWriters賦值。
// 獲取一個寫入對象,其實是DiskBlockObjectWriter對象,保存到partitionWriters數組中
partitionWriters[i] =
blockManager.getDiskWriter(blockId, file, serInstance, fileBufferSize, writeMetrics);
注意:這裏balockId是一個uuid,保證每個分區的shuffle文件名不重複。
- 把分區數據按key-value的方式寫入流中:
partitionWriters[partitioner.getPartition(key)].write(key, record._2());
- 讓輸出流中的數據全部刷新到磁盤文件中:
partitionWriterSegments[i] = writer.commitAndGet();
- 合併以上的每個分區的臨時文件。
先創建一個結果文件對象output,再創建一個tmp文件對象,會先把所有分區文件的內容都合併到tmp文件中,然後再把tmp文件rename成output的文件名。
File output = shuffleBlockResolver.getDataFile(shuffleId, mapId);
File tmp = Utils.tempFileWith(output);
try {
// 把臨時文件對象的內容合併成一個單獨的文件,返回每個分區的長度的數組
partitionLengths = writePartitionedFile(tmp);
// 把數據持久化到磁盤上,然後把tmp文件rename成output文件
shuffleBlockResolver.writeIndexFileAndCommit(shuffleId, mapId, partitionLengths, tmp);
} finally {
if (tmp.exists() && !tmp.delete()) {
logger.error("Error while deleting temp file {}", tmp.getAbsolutePath());
}
}
- 最後刪除臨時分區的中間文件。
writePartitionedFile函數的實現
該函數把每個分區生成的文件,合併成一個單個文件。
- 根據所給的參數創建一個文件輸出流對象:out
- 遍歷分區,獲取在寫入分區數據時生成的文件對象,並生成文件輸入流:
final FileInputStream in = new FileInputStream(file);
- 通過一個Utils.copyStream函數把輸入流的數據複製到輸出流中,若設置了參數:spark.file.transferTo,該函數會使用NIO的方式來複制流數據。
- 完成以上操作後,刪除爲每個分區創建的臨時文件。
查看BypassMergeShuffleWriter的實際使用
scala> val rdd = sc.parallelize(0 to 8).groupBy(_ % 3)
rdd: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[2] at groupBy at <console>:24
scala> rdd.dependencies
res0: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@3a7be6e6)
scala> rdd.getNumPartitions
res1: Int = 8
scala> import org.apache.spark.ShuffleDependency
import org.apache.spark.ShuffleDependency
scala> val shuffleDep = rdd.dependencies(0).asInstanceOf[ShuffleDependency[Int, Int, Int]]
shuffleDep: org.apache.spark.ShuffleDependency[Int,Int,Int] = org.apache.spark.ShuffleDependency@3a7be6e6
scala> shuffleDep.mapSideCombine
res2: Boolean = false
scala> shuffleDep.aggregator
res3: Option[org.apache.spark.Aggregator[Int,Int,Int]] = Some(Aggregator(<function1>,<function2>,<function2>))
scala> shuffleDep.partitioner.numPartitions
res4: Int = 8
scala> shuffleDep.shuffleHandle
res5: org.apache.spark.shuffle.ShuffleHandle = org.apache.spark.shuffle.sort.BypassMergeSortShuffleHandle@214738c1
總結
通過以上分析可知,BypassMergeShuffleWriter的使用有一定的條件:
- map端沒有聚合操作
- 處理數據分區數小於參數spark.shuffle.sort.bypassMergeThreshold的值。
而當啓用BypassMergeShuffleWriter時,需要注意:
- 它會爲每個分區創建一個磁盤寫入的對象,和一個文件寫入流對象
- 它會先爲每個分區數據創建一個臨時文件
- 當數據的分區太大時,創建的輸出流對象和臨時文件將會很多,佔用的資源可能會很多,可能導致任務性能降低。