前言
上篇寫了 Spark Shuffle 內(nèi)存分析 后,有不少人提出了疑問,大家也對如何落文件挺感興趣的,所以這篇文章會詳細介紹,Sort Based Shuffle Write 階段是如何進行落磁盤的
流程分析
入口處:
org.apache.spark.scheduler.ShuffleMapTask.runTask
runTask對應(yīng)的代碼為:
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的。我們分析的線路假設(shè)需要做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當(dāng)中:
sorter.insertAll(records)
其中insertAll 的流程是這樣的:
while (records.hasNext) {
addElementsRead() kv = records.next()
map.changeValue((getPartition(kv._1), kv._1), update)
maybeSpillCollection(usingMap = true)}
里面的map 其實就是PartitionedAppendOnlyMap,這個是全內(nèi)存的一個結(jié)構(gòu)。當(dāng)把這個寫滿了,才會觸發(fā)spill操作。你可以看到maybeSpillCollection在PartitionedAppendOnlyMap每次更新后都會被調(diào)用。
一旦發(fā)生呢個spill后,產(chǎn)生的文件名稱是:
"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))
}
產(chǎn)生的所有 spill文件被被記錄在一個數(shù)組里:
private val spills = new ArrayBuffer[SpilledFile]
迭代完一個task對應(yīng)的partition數(shù)據(jù)后,會做merge操作,把磁盤上的spill文件和內(nèi)存的,迭代處理,得到一個新的iterator,這個iterator的元素會是這個樣子的:
(p, mergeWithAggregation(
iterators,
aggregator.get.mergeCombiners, keyComparator,
ordering.isDefined))
其中p 是reduce 對應(yīng)的partitionId, p對應(yīng)的所有數(shù)據(jù)都會在其對應(yīng)的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 其實內(nèi)部元素是reduce partitionID -> 實際record 的 iterator,所以它其實是順序?qū)懨總€分區(qū)的記錄,寫完形成一個fileSegment,并且記錄偏移量。這樣后續(xù)每個的reduce就可以根據(jù)偏移量拿到自己需要的數(shù)據(jù)。對應(yīng)的文件名,前面也提到了,是:
"shuffle_" + shuffleId + "_" + mapId + "_" + NOOP_REDUCE_ID + ".data"
剛剛我們說偏移量,其實是存在內(nèi)存里的,所以接著要持久化,通過下面的writeIndexFile來完成:
shuffleBlockResolver.writeIndexFile(
dep.shuffleId,
mapId,
partitionLengths)
具體的文件名是:
"shuffle_" + shuffleId + "_" + mapId + "_" + NOOP_REDUCE_ID + ".index"
至此,一個task的寫入操作完成,對應(yīng)一個文件。
最終結(jié)論
所以最后的結(jié)論是,一個Executor 最終對應(yīng)的文件數(shù)應(yīng)該是:
MapNum (注:不包含index文件)
同時持有并且會進行寫入的文件數(shù)最多為::
CoreNum