Spark Shuffle Write階段磁碟檔案分析

westwolf發表於2021-09-09

前言

上篇寫了 後,有不少人提出了疑問,大家也對如何落檔案挺感興趣的,所以這篇文章會詳細介紹,Sort Based Shuffle Write 階段是如何進行落磁碟的

流程分析

入口處:

org.apache.spark.scheduler.ShuffleMapTask.runTask

runTask對應的程式碼為:

val manager = SparkEnv.get.shuffleManager
writer = manager.getWriter[Any, Any](
                              dep.shuffleHandle, 
                              partitionId, 
                              context)
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
writer.stop(success = true).get

這裡manager 拿到的是

   org.apache.spark.shuffle.sort.SortShuffleWriter

我們看他是如何拿到可以寫磁碟的那個sorter的。我們分析的線路假設需要做mapSideCombine

 sorter = if (dep.mapSideCombine) {  
 require(dep.aggregator.isDefined, "Map-side combine without Aggregator specified!")  
 new ExternalSorter[K, V, C](
                 dep.aggregator, 
                 Some(dep.partitioner), 
                 dep.keyOrdering, de.serializer)

接著將map的輸出放到sorter當中:

sorter.insertAll(records)

其中insertAll 的流程是這樣的:

 while (records.hasNext) {  
 addElementsRead()  kv = records.next() 
 map.changeValue((getPartition(kv._1), kv._1), update)
 maybeSpillCollection(usingMap = true)}

裡面的map 其實就是PartitionedAppendOnlyMap,這個是全記憶體的一個結構。當把這個寫滿了,才會觸發spill操作。你可以看到maybeSpillCollection在PartitionedAppendOnlyMap每次更新後都會被呼叫。

一旦發生呢個spill後,產生的檔名稱是:

    "temp_shuffle_" + id

邏輯在這:

val (blockId, file) = diskBlockManager.createTempShuffleBlock() 

  def createTempShuffleBlock(): (TempShuffleBlockId, File) = {  
  var blockId = new TempShuffleBlockId(UUID.randomUUID()) 
        while (getFile(blockId).exists()) {   
           blockId = new TempShuffleBlockId(UUID.randomUUID())  
        }  
  (blockId, getFile(blockId))
  }

產生的所有 spill檔案被被記錄在一個陣列裡:

  private val spills = new ArrayBuffer[SpilledFile]

迭代完一個task對應的partition資料後,會做merge操作,把磁碟上的spill檔案和記憶體的,迭代處理,得到一個新的iterator,這個iterator的元素會是這個樣子的:

 (p, mergeWithAggregation(  
             iterators, 
             aggregator.get.mergeCombiners, keyComparator,             ordering.isDefined))

其中p 是reduce 對應的partitionId, p對應的所有資料都會在其對應的iterator中。

接著會獲得最後的輸出檔名:

val outputFile = shuffleBlockResolver.getDataFile(dep.shuffleId, mapId)

檔名格式會是這樣的:

 "shuffle_" + shuffleId + "_" + mapId + "_" + reduceId + ".data"

其中reduceId 是一個固定值NOOP_REDUCE_ID,預設為0。

然後開始真實寫入檔案

   val partitionLengths = sorter.writePartitionedFile(
     blockId, 
     context, 
     outputFile)

寫入檔案的過程過程是這樣的:

for ((id, elements) <- this.partitionedIterator) { 
 if (elements.hasNext) {   
 
val writer = blockManager.getDiskWriter(blockId,
      outputFile, 
      serInstance,
      fileBufferSize,  
      context.taskMetrics.shuffleWriteMetrics.get)   

for (elem <- elements) {     
     writer.write(elem._1, elem._2)   
 }   
 
writer.commitAndClose()    
val segment = writer.fileSegment()   
lengths(id) = segment.length  
   }
}

剛剛我們說了,這個 this.partitionedIterator 其實內部元素是reduce partitionID -> 實際record 的 iterator,所以它其實是順序寫每個分割槽的記錄,寫完形成一個fileSegment,並且記錄偏移量。這樣後續每個的reduce就可以根據偏移量拿到自己需要的資料。對應的檔名,前面也提到了,是:

"shuffle_" + shuffleId + "_" + mapId + "_" + NOOP_REDUCE_ID + ".data"

剛剛我們說偏移量,其實是存在記憶體裡的,所以接著要持久化,透過下面的writeIndexFile來完成:

 shuffleBlockResolver.writeIndexFile(           dep.shuffleId,           mapId, 
          partitionLengths)

具體的檔名是:

  "shuffle_" + shuffleId + "_" + mapId + "_" + NOOP_REDUCE_ID + ".index"

至此,一個task的寫入操作完成,對應一個檔案。

最終結論

所以最後的結論是,一個Executor 最終對應的檔案數應該是:

MapNum (注:不包含index檔案)

同時持有並且會進行寫入的檔案數最多為::

 CoreNum



作者:祝威廉
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4662/viewspace-2818600/,如需轉載,請註明出處,否則將追究法律責任。

相關文章