25.Spark Sort-Based Shuffle內幕徹底解密

一:爲什麼需要Sort-BasedShuffle?

1,  Shuffle一般包含兩個階段任務:

第一部分:產生Shuffle數據的階段(Map階段,額外補充,需要實現ShuffleManager中的getWriter來寫數據(數據可以通過BlockManager寫到Memory,Disk,Tachyon等,例如想非常快的Shuffle,此時可以考慮把數據寫在內存中,但是內存不穩定,所以可以考慮增加副本。建議採用MEMONY_AND_DISK方式);

第二部分:使用Shuffle數據的階段(Reduce階段,額外補充,Shuffle讀數據:需要實現ShuffleManager的getReader,Reader會向Driver去獲取上一個Stage產生的Shuffle數據)。

2,  Spark的Job會被劃分成很多Stage:

如果只要一個Stage,則這個Job就相當於只有一個Mapper段,當然不會產生Shuffle,適合於簡單的ETL。如果不止一個Stage,則最後一個Stage就是最終的Reducer,最左側的第一個Stage就僅僅是整個Job的Mapper,中間所有的任意一個Stage是其父Stage的Reducer且是其子Stage的Mapper;

二:Hash-basedShuf

1,  Spark Shuffle在最開始的時候只支持Hash-based Shuffle:默認Mapper階段會爲Reducer階段的每一個Task單獨創建一個文件來保存該Task中要使用的數據。

優點:就是操作數據簡單。

缺點:但是在一些情況下(例如數據量非常大的情況)會造成大量文件(M*R,其中M代表Mapper中的所有的並行任務數量,R代表Reducer中所有的並行任務數據)大數據的隨機磁盤I/O操作且會形成大量的Memory(極易造成OOM)。

2,Hash-based Shuffle產生的問題:

第一:不能夠處理大規模的數據

第二:Spark不能夠運行在大規模的分佈式集羣上!

  3,Consolidate機制:

後來的改善是加入了Consolidate機制來將Shuffle時候產生的文件數量減少到C*R個(C代表在Mapper端,同時能夠使用的cores數量,R代表Reducer中所有的並行任務數量)。但是此時如果Reducer端的並行數據分片過多的話則C*R可能已經過大,此時依舊沒有逃脫文件打開過多的厄運!!!Consolidate並沒有降低並行度,只是降低了臨時文件的數量,此時Mapper端的內存消耗就會變少,所以OOM也就會降低,另外一方面磁盤的性能也會變得更好。

Spark在引入Sort-Based Shuffle之前,適合中小型數據規模的大數據處理!

三:Sort-BasedShuffle

1,爲了讓Spark在更大規模的集羣上更高性能處理更大規模的數據,於是就引入了Sort-based Shuffle!從此以後(Spark1.1版本開始),Spark可以勝任任何規模(包括PB級別及PB以上的級別)的大數據的處理,尤其是鎢絲計劃的引入和優化,Spark更快速的在更大規模的集羣處理更海量的數據的能力推向了一個新的巔峯!

2,Spark1.6版本支持最少三種類型Shuffle:

// Letthe user specify short names for shuffle managers
val shortShuffleMgrNames =Map(
"hash" ->"org.apache.spark.shuffle.hash.HashShuffleManager",
"sort" ->"org.apache.spark.shuffle.sort.SortShuffleManager",
"tungsten-sort"->"org.apache.spark.shuffle.sort.SortShuffleManager")
val shuffleMgrName = conf.get("spark.shuffle.manager","sort")
val shuffleMgrClass =shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase,shuffleMgrName)
val shuffleManager =instantiateClass[ShuffleManager](shuffleMgrClass)

實現ShuffleManager接口可以根據自己的業務實際需要最優化的使用自定義的Shuffle實現;

 3,Spark1.6默認採用的就是Sort-basedShuffle的方式:

val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")

上述源碼說明,你可以在Spark配置文件中配置Spark框架運行時要使用的具體的ShuffleManager的實現。可以在conf/spark-default.conf加入如下內容:

      spark.shuffle.managerSORT   配置Shuffle方式是SORT

3,  Sort-based Shuffle的工作方式如下:Shuffle的目的就是:數據分類,然後數據聚集

1)      首先每個ShuffleMapTask不會爲每個Reducer單獨生成一個文件,相反,Sort-based Shuffle會把Mapper中每個ShuffleMapTask所有的輸出數據Data只寫到一個文件中。因爲每個ShuffleMapTask中的數據會被分類,所以Sort-based Shuffle使用了index文件存儲具體ShuffleMapTask輸出數據在同一個Data文件中是如何分類的信息!!

2)      基於Sort-base的Shuffle會在Mapper中的每一個ShuffleMapTask中產生兩個文件:Data文件和Index文件,其中Data文件是存儲當前Task的Shuffle輸出的。而index文件中則存儲了Data文件中的數據通過Partitioner的分類信息,此時下一個階段的Stage中的Task就是根據這個Index文件獲取自己所要抓取的上一個Stage中的ShuffleMapTask產生的數據的,Reducer就是根據index文件來獲取屬於自己的數據。

涉及問題:Sorted-basedShuffle:會產生 2*M(M代表了Mapper階段中並行的Partition的總數量,其實就是ShuffleMapTask的總數量)個Shuffle臨時文件。

Shuffle產生的臨時文件的數量的變化一次爲:

                  Basic Hash Shuffle: M*R;

                  Consalidate方式的Hash Shuffle: C*R;

                  Sort-based Shuffle: 2*M;

四:Sort-based Shuffle 實驗

在Sorted-based Shuffle中Reducer是如何獲取自己需要的數據呢?具體而言,Reducer首先找Driver去獲取父Stage中的ShuffleMapTask輸出的位置信息,根據位置信息獲取index文件,解析index,從解析的index文件中獲取Data文件中屬於自己的那部分內容;

Sorted-basedShuffle與排序沒有關係,Sorted-based Shuffle並沒有對內容進行排序,Sorted-basedShuffle是對Shuffle進行Sort,對我們具體要執行的內容沒有排序。

Reducer在什麼時候去fetch數據?

當parent Stage的所有ShuffleMapTasks結束後再fetch。等所有的ShuffleMapTask執行完之後,邊fetch邊計算。

通過動手實踐確實證明了Sort-based Shuffle產生了2M個文件。M是並行Task的數量。

Shuffle_0_0_0.data

           shuffle_0_3_0.index

從上可以看出index文件和data文件數量是一樣的。

Sorted Shuffle Writer源碼:

1.      ShuffleMapTask的runTask方法

反序列化RDD和Dependency

調用SortShuffleManager的getWriter方法。

Writer方法寫入結果。

overridedefrunTask(context: TaskContext): MapStatus = {
// Deserialize the RDDusing the broadcast variable.
val deserializeStartTime = System.currentTimeMillis()
val ser = SparkEnv.get.closureSerializer.newInstance()

//反序列化獲得RDD和Dependency
val (rdd,dep) = ser.deserialize[(RDD[_],ShuffleDependency[_,_, _])](
    ByteBuffer.wrap(taskBinary.value)
,Thread.currentThread.getContextClassLoader)
_executorDeserializeTime= System.currentTimeMillis()- deserializeStartTime

metrics =Some(context.taskMetrics)
var writer: ShuffleWriter[Any,Any] = null
  try
{

//獲得SortShuffleManager
val manager = SparkEnv.get.shuffleManager

//獲得SortShuffleManager的getWriter方法,獲得partitionId
    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

2.      SortShuffleManager複寫了ShuffleManager中的getWriter方法,源碼如下:

/**Get a writer for a given partition. Called on executors by map tasks. */
override def getWriter[K,V](
    handle: ShuffleHandle
,
mapId: Int,        //也就是partitionId
context:TaskContext): ShuffleWriter[K,V] = {
numMapsForShuffle.putIfAbsent(

//中間代碼省略
case other: BaseShuffleHandle[K@unchecked,V @unchecked,_] =>

//創建SortShuffleWriter對象

//shuffleBlockResolver:index文件
new SortShuffleWriter(shuffleBlockResolver,other, mapId, context)
  }

3.      SorShuffleWriter的write方法源碼如下:

/**Write a bunch of records to this task's output */
override def write(records:Iterator[Product2[K,V]]): Unit = {
sorter =if (dep.mapSideCombine) {
require(
dep.aggregator.isDefined,"Map-side combine without Aggregatorspecified!")

//創建ExternalSorter實例。
new ExternalSorter[K,V, C](
      context
, dep.aggregator,Some(dep.partitioner),dep.keyOrdering,dep.serializer)
  }
else {
// In this case wepass neither an aggregator nor an ordering to the sorter, because we don't
    // care whether the keys get sortedin each partition; that will be done on the reduce side
    // if the operation being run issortByKey.
new ExternalSorter[K,V, V](
      context
, aggregator = None,Some(dep.partitioner),ordering = None,dep.serializer)
  }

//然後將結果通過insertAll寫入緩存中。
sorter.insertAll(records)

// Don't botherincluding the time to open the merged output file in the shuffle write time,
  // because it just opens a single file,so is typically too fast to measure accurately
  // (see SPARK-3570).

//獲得當前文件的輸出路徑
val output =shuffleBlockResolver.getDataFile(dep.shuffleId,mapId)
val tmp = Utils.tempFileWith(output)

//創建BlockId
val blockId =ShuffleBlockId(dep.shuffleId,mapId, IndexShuffleBlockResolver.NOOP_REDUCE_ID)

//調用ExternalSorter.writePartitionedFile將中間結果持久化
val partitionLengths =sorter.writePartitionedFile(blockId,tmp)

//創建索引文件。
 shuffleBlockResolver.writeIndexFileAndCommit(
dep.shuffleId,mapId, partitionLengths, tmp)
mapStatus =MapStatus(blockManager.shuffleServerId,partitionLengths)
}

其中ShuffleBlockId記錄shuffleId和mapId獲得Block。

//Format of the shuffle block ids (including data and index) should be kept insync with
//org.apache.spark.network.shuffle.ExternalShuffleBlockResolver#getBlockData().
@DeveloperApi
case class ShuffleBlockId(shuffleId:Int, mapId:Int, reduceId:Int) extends BlockId {

//ShuffleBlockId的格式如下:
override def name:String = "shuffle_" + shuffleId +"_" + mapId +"_" + reduceId
}

4.      其中writeIndexFileAndCommit方法:

用於在Block的索引文件中記錄每個block的偏移量,其中getBlockData方法可以根據ShuffleId和mapId讀取索引文件,獲得前面partition計算之後,,將結果寫入文件中的偏移量和結果的大小。

/**
 * Write an index file with the offsets of each block, plus a final offset at the end for the
 * end of the output file. This will be used by getBlockData to figure out where each block
 * begins and ends.
 *
 * It will commit the data and index file as an atomic operation, use the existing ones, or
 * replace them with new ones.
 *
 * Note: the `lengths` will be updated to match the existing index file if use the existing ones.
 * */
def writeIndexFileAndCommit(
    shuffleId: Int,
mapId: Int,
lengths: Array[Long],
dataTmp: File): Unit = {
val indexFile = getIndexFile(shuffleId, mapId)
val indexTmp = Utils.tempFileWith(indexFile)
val out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(indexTmp)))
  Utils.tryWithSafeFinally {
// We take in lengths of each block, need to convert it to offsets.
var offset = 0L
out.writeLong(offset)
for (length <- lengths) {
      offset += length
      out.writeLong(offset)
    }
  } {
    out.close()
  }

val dataFile = getDataFile(shuffleId, mapId)
// There is only one IndexShuffleBlockResolver per executor, this synchronization make sure
  // the following check and rename are atomic.
synchronized {
val existingLengths = checkIndexAndDataFile(indexFile, dataFile, lengths.length)
if (existingLengths != null) {
// Another attempt for the same task has already written our map outputs successfully,
      // so just use the existing partition lengths and delete our temporary map outputs.
System.arraycopy(existingLengths, 0, lengths, 0, lengths.length)
if (dataTmp != null && dataTmp.exists()) {
        dataTmp.delete()
      }
      indexTmp.delete()
    } else {
// This is the first successful attempt in writing the map outputs for this task,
      // so override any existing index and data files with the ones we wrote.
if (indexFile.exists()) {
        indexFile.delete()
      }
if (dataFile.exists()) {
        dataFile.delete()
      }
if (!indexTmp.renameTo(indexFile)) {
throw new IOException("fail to rename file " + indexTmp + " to " + indexFile)
      }
if (dataTmp != null && dataTmp.exists() && !dataTmp.renameTo(dataFile)) {

總結如下:

三:默認Sort-based Shuffle的幾個缺陷:

1.     如果Mapper中Task的數量過大,依舊會產生很多小文件,此時在Shuffle傳遞數據的過程中到Reducer端,reduce會需要同時打開大量的記錄來進行反序列化,導致大量的內存消耗和GC的巨大負擔,造成系統緩慢甚至崩潰!

2.如果需要在分片內也進行排序的話,此時需要進行Mapper端和Reducer端的兩次排序!!!

優化:

         可以改造Mapper和Reducer端,改框架來實現一次排序。

         頻繁GC的解決辦法是:鎢絲計劃!!


 

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