SparkCore-Architecture-5

fengye發表於2019-02-19

本系列文章源自JerryLead的SparkInternals,本文只是在作者的原文基礎上加入自己的理解,批註,和部分原始碼,作為學習之用
注:原文是基於Spark 1.0.2 , 而本篇筆記是基於spark 2.2.0, 對比後發現核心部分變化不大,依舊值得參考

架構

前三章從 job 的角度介紹了使用者寫的 program 如何一步步地被分解和執行。這一章主要從架構的角度來討論 master,worker,driver 和 executor 之間怎麼協調來完成整個 job 的執行。

實在不想在文件中貼過多的程式碼,這章貼這麼多,只是為了方面自己回頭 debug 的時候可以迅速定位,不想看程式碼的話,直接看圖和描述即可。

部署圖

重新貼一下 Overview 中給出的部署圖:

deploy

接下來分階段討論並細化這個圖。

Job 提交

下圖展示了driver program(假設在 master node 上執行)如何生成 job,並提交到 worker node 上執行。

JobSubmission

Driver 端的邏輯如果用程式碼表示:

finalRDD.action()
=> sc.runJob()

// generate job, stages and tasks
=> dagScheduler.runJob()
=> dagScheduler.submitJob()
//將任務提交JobSubmitted放置在event佇列當中,eventThread後臺執行緒將對該任務提交進行處理
=>    dagSchedulerEventProcessLoop.post(JobSubmitted)

//實際上並不是接收外部訊息,而是執行緒從阻塞佇列裡獲取結果並匹配執行
DAGSchedulerEventProcessLoop.onReceive()
=>  dagScheduler.handleJobSubmitted(jobId, ...)
=>      finalStage = createResultStage(finalRDD...)
            getShuffleDependencies()
                //用stack做深度優先遍歷
                toVisit.dependencies.foreach {
                  //如果是ShuffleDependency,則為parent
                  case shuffleDep: ShuffleDependency[_, _, _] =>
                    parents += shuffleDep
                    //同一個stage,加入尋找佇列,要一直找到所有的parent stage
                  case dependency =>
                    waitingForVisit.push(dependency.rdd)
                }
              getOrCreateShuffleMapStage()    
                 createShuffleMapStage()
                    new ShuffleMapStage()
                    mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
=>      submitStage(finalStage)
複製程式碼
//父stage就遞迴呼叫,若沒有父stage就提交當前stage,這種邏輯就要找到頂層stage提交
dagScheduler.submitStage(stage: Stage)
    //獲取父stage,如果 parentStages 都可能已經執行過了,那麼就為空了
    val missing = getMissingParentStages(stage) 
        //stage 劃分演算法,基於stack操作,寬依賴建立ShuffleMapStage,
        case shufDep: ShuffleDependency[_, _, _] => getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
        //窄依賴加入stack,一定要追溯到其依賴的ShuffleDependency/none
        case narrowDep: NarrowDependency[_] =>  waitingForVisit.push(narrowDep.rdd)
    submitMissingTasks(stage, jobId.get)//沒有父stage就提交task
    submitStage(parent)//若有父stage就遞迴提交所有父stage,然後再提交子stage
複製程式碼
dagScheduler.submitMissingTasks(stage: Stage, jobId: Int)
    taskBinary = sc.broadcast(taskBinaryBytes)//廣播task
    //按照stage的型別,生成相應的ShuffleMapTask/ResultTask
    val tasks: Seq[Task[_]] = new ShuffleMapTask / new ResultTask
    //提交taskset
=>  taskScheduler.submitTasks(new TaskSet(...))
       val manager = createTaskSetManager(taskSet, maxTaskFailures)
       //將TaskSetManager加入rootPool排程池中,由schedulableBuilder決定排程順序
       schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
       //呼叫SchedulerBackend的reviveOffers方法對Task進行排程,決定task具體執行在哪個Executor中
       //對Task進行排程,決定task具體執行在哪個Executor中
=>      schedulerBackend.reviveOffers()
          driverEndpoint.send(ReviveOffers) //給driver傳送排程資訊

//driver端排程執行task          
CoarseGrainedSchedulerBackend.receive()
    case ReviveOffers =>
=>      makeOffers()
            //考慮 locality 等因素來確定 task 的全部資訊 TaskDescription
            scheduler.resourceOffers(workOffers)
=>          launchTasks()
                 //向executor傳送訊息,在executor上啟動task
                 foreach task 
                    executorEndpoint.send(LaunchTask())
複製程式碼

程式碼的文字描述:

當使用者的 program 呼叫 val sc = new SparkContext(sparkConf) 時,這個語句會幫助 program 啟動諸多有關 driver 通訊、job 執行的物件、執行緒等,該語句確立了 program 的 driver 地位。

生成 Job 邏輯執行圖

Driver program 中的 transformation() 建立 computing chain(一系列的 RDD),每個 RDD 的 compute() 定義資料來了怎麼計算得到該 RDD 中 partition 的結果,getDependencies() 定義 RDD 之間 partition 的資料依賴。

生成 Job 物理執行圖

每個 action() 觸發生成一個 job,在 dagScheduler.runJob() 的時候進行 stage 劃分,在 submitStage() 的時候生成該 stage 包含的具體的 ShuffleMapTasks 或者 ResultTasks,然後將 tasks 打包成 TaskSet 交給 taskScheduler,如果 taskSet 可以執行就將 tasks 交給 CoarseGrainedSchedulerBackend 去分配執行。

分配 Task

CoarseGrainedSchedulerBackend 接收到 taskSet 後,會通 將 serialized tasks 傳送到排程器指定的 worker node 上的 CoarseGrainedExecutorBackend Endpoint上。

Job 接收

Worker 端接收到 tasks 後,執行如下操作

//driver端先發LaunchTask資訊給executor
executorEndpoint.send(LaunchTask(serializedTask)
=> executor.launchTask()
    // Executors.newCachedThreadPool,用的是無界佇列
=> executor.threadPool.execute(new TaskRunner(context, taskDescription))
複製程式碼

executor 將 task 包裝成 taskRunner,並從執行緒池中抽取出一個空閒執行緒執行 task。一個 CoarseGrainedExecutorBackend 程式有且僅有一個 executor 物件。

Task 執行

下圖展示了 task 被分配到 worker node 上後的執行流程及 driver 如何處理 task 的 result。

TaskExecution

Executor 收到 serialized 的 task 後,先 deserialize 出正常的 task,然後執行 task 得到其執行結果 directResult,這個結果要送回到 driver 那裡。但是通過 Actor 傳送的資料包不宜過大,如果 result 比較大(比如 groupByKey 的 result)先把 result 存放到本地的“記憶體+磁碟”上,由 blockManager 來管理,只把儲存位置資訊(indirectResult)傳送給 driver,driver 需要實際的 result 的時候,會通過 HTTP 去 fetch。如果 result 不大(小於spark.akka.frameSize = 10MB),那麼直接傳送給 driver。

上面的描述還有一些細節:如果 task 執行結束生成的 directResult > akka.frameSize,directResult 會被存放到由 blockManager 管理的本地“記憶體+磁碟”上。BlockManager 中的 memoryStore 開闢了一個 LinkedHashMap 來儲存要存放到本地記憶體的資料。LinkedHashMap 儲存的資料總大小不超過 Runtime.getRuntime.maxMemory * spark.storage.memoryFraction(default 0.6) 。如果 LinkedHashMap 剩餘空間不足以存放新來的資料,就將資料交給 diskStore 存放到磁碟上,但前提是該資料的 storageLevel 中包含“磁碟”。

這裡directResult傳輸的大小標準已經改為Math.min((“spark.rpc.message.maxSize”, 128m),(“spark.task.maxDirectResultSize”, 1L << 20))

上文的記憶體空間管理需要再驗證

In TaskRunner.run()
=> coarseGrainedExecutorBackend.statusUpdate(TaskState.RUNNING)
        driverRef.send(StatusUpdate)
=> updateDependencies(addedFiles, addedJars) //下載依賴資源
=> task = ser.deserialize(serializedTask)
=> value = task.run(taskId) //會呼叫子類ShuffleMapTask/ResultTask的runTask方法
=> directResult = new DirectTaskResult(ser.serialize(value),accumUpdates)   //Accumulator
=> if( resultSize > maxResultSize )  //預設maxResultSize是1G
       //IndirectTaskResult是woker BlockManager中儲存DirectTaskResult的引用
        ser.serialize(new IndirectTaskResult[Any](TaskResultBlockId(taskId), resultSize))
   else if (resultSize > maxDirectResultSize) {  //若task返回結果大於128M(預設的rpc傳輸訊息大小) < 1G
        ser.serialize(new IndirectTaskResult[Any](blockId, resultSize))
    }else{
        ser.serialize(directResult)
    }
=> coarseGrainedExecutorBackend.statusUpdate(TaskState.FINISHED,result)
=>      driverRef.send(StatusUpdate,result)
複製程式碼

ShuffleMapTask 和 ResultTask 生成的 result 不一樣。ShuffleMapTask 生成的是 MapStatus,MapStatus 包含兩項內容:一是該 task 所在的 BlockManager 的 BlockManagerId(實際是 executorId + host, port, nettyPort),二是 task 輸出的每個 FileSegment 大小。ResultTask 生成的 result 的是 func 在 partition 上的執行結果。比如 count() 的 func 就是統計 partition 中 records 的個數。由於 ShuffleMapTask 需要將 FileSegment 寫入磁碟,因此需要輸出流 writers,這些 writers 是由 blockManger 裡面的 shuffleBlockManager 產生和控制的。

In task.run(taskId) //會呼叫子類的runTask方法
// if the task is ShuffleMapTask
=> shuffleMapTask.runTask(context)
=> shuffleManager.getWriter.write(rdd.iterator(partition, context))
//MapStatus包含了task將shuffle檔案的寫入地址
=> return MapStatus(blockManager.blockManagerId, Array[compressedSize(fileSegment)])

//If the task is ResultTask,直接執行
=> return func(context, rdd.iterator(split, context))
複製程式碼

Driver 收到 task 的執行結果 result 後會進行一系列的操作:首先告訴 taskScheduler 這個 task 已經執行完,然後去分析 result。由於 result 可能是 indirectResult,需要先呼叫 blockManager.getRemoteBytes() 去 fech 實際的 result,這個過程下節會詳解。得到實際的 result 後,需要分情況分析,如果是 ResultTask 的 result,那麼可以使用 ResultHandler 對 result 進行 driver 端的計算(比如 count() 會對所有 ResultTask 的 result 作 sum),如果 result 是 ShuffleMapTask 的 MapStatus,那麼需要將 MapStatus(ShuffleMapTask 輸出的 FileSegment 的位置和大小資訊)存放到 mapOutputTrackerMaster 中的 mapStatuses 資料結構中以便以後 reducer shuffle 的時候查詢。如果 driver 收到的 task 是該 stage 中的最後一個 task,那麼可以 submit 下一個 stage,如果該 stage 已經是最後一個 stage,那麼告訴 dagScheduler job 已經完成。

After driver receives StatusUpdate(result)
=> taskSchedulerImpl.statusUpdate(taskId, state, result.value)
//TaskState.isFinished(state) && state == TaskState.FINISHED
=> taskResultGetter.enqueueSuccessfulTask(taskSet, tid, result)
        TaskResultExecutor.execute.(new Runnable().run())
            if result is directResult
                directResult.value(serializer)
            if result is IndirectResult
                serializedTaskResult = blockManager.getRemoteBytes(blockId)
=>          taskSchedulerImpl.handleSuccessfulTask(taskSetManager, tid, result)
                //Marks a task as successful and notifies the DAGScheduler that the task has ended.
=>              taskSetManager.handleSuccessfulTask(tid, taskResult)
                    sched.backend.killTask() //殺死所有其他與之相同的task的嘗試
                    //通知dagScheduler該task完成
=>                  dagScheduler.taskEnded(result.value, result.accumUpdates)
                        eventProcessLoop.post(CompletionEvent)  //起執行緒處理的

dagScheduler.doOnReceive()
    dagScheduler.handleTaskCompletion(completion)
=>      if task Success
            if task is ResultTask
                updateAccumulators(event)
                if (job.numFinished == job.numPartitions) 
                    markStageAsFinished(resultStage)
                    //Removes state for job and any stages that are not needed by any other job
                    cleanupStateForJobAndIndependentStages(job)
                    listenerBus.post(SparkListenerJobEnd(job.jobId, JobSucceeded))
                job.listener.taskSucceeded(outputId, result)//通知 JobWaiter 有任務成功
                    jobWaiter.taskSucceeded(index, result)
                    resultHandler(index, result)

             if task is ShuffleMapTask
                updateAccumulators(event)
                 shuffleStage.pendingPartitions -= task.partitionId
                 shuffleStage.addOutputLoc(smt.partitionId, mapStatus)
                if (all tasks in current stage have finished)
                    mapOutputTracker.registerMapOutputs(shuffleId, Array[MapStatus])
                        mapStatuses.put(shuffleId, Array[MapStatus])
=>              submitWaitingChildStages(stage)
                    waitingStages.filter(_.parents.contains(parent)).foreach.submitStage(_)
        
        //補充下其他可能的情況            
=>      if task Resubmitted      
            pendingPartitions += task.partitionId //TaskSetManagers只對ShuffleMapStage Resubmitted
            
=>      if task FetchFailed
            if fail times > (spark.stage.maxConsecutiveAttempts,4)
                abortStage(failedStage, abortMessage)
            else new Runnable.run(){eventProcessLoop.post(ResubmitFailedStages)}
            handleExecutorLost(executorId)//有多次fetch failures 就標記executor丟失
                blockManagerMaster.removeExecutor(execId)
                foreach ShuffleMapStage in executor
                    stage.removeOutputsOnExecutor(execId)
                    mapOutputTracker.registerMapOutputs(shuffleId,Array[MapStatus])
        
=>      if task exceptionFailure  
            // 異常還要更新accumulator~~
             updateAccumulators(event)
複製程式碼

Shuffle read

上一節描述了 task 執行過程及 result 的處理過程,這一節描述 reducer(需要 shuffle 的 task )是如何獲取到輸入資料的。關於 reducer 如何處理輸入資料已經在上一章的 shuffle read 中解釋了。

問題:reducer 怎麼知道要去哪裡 fetch 資料?

readMapStatus

reducer 首先要知道 parent stage 中 ShuffleMapTask 輸出的 FileSegments 在哪個節點。這個資訊在 ShuffleMapTask 完成時已經送到了 driver 的 mapOutputTrackerMaster,並存放到了 mapStatuses: HashMap<stageId, Array[MapStatus]> 裡面,給定 stageId,可以獲取該 stage 中 ShuffleMapTasks 生成的 FileSegments 資訊 Array[MapStatus],通過 Array(taskId) 就可以得到某個 task 輸出的 FileSegments 位置(blockManagerId)及每個 FileSegment 大小。

當 reducer 需要 fetch 輸入資料的時候,會首先呼叫 blockStoreShuffleFetcher 去獲取輸入資料(FileSegments)的位置。blockStoreShuffleFetcher 通過呼叫本地的 MapOutputTrackerWorker 去完成這個任務,MapOutputTrackerWorker 使用 mapOutputTrackerMasterActorRef 來與 mapOutputTrackerMasterActor 通訊獲取 MapStatus 資訊。blockStoreShuffleFetcher 對獲取到的 MapStatus 資訊進行加工,提取出該 reducer 應該去哪些節點上獲取哪些 FileSegment 的資訊,這個資訊存放在 blocksByAddress 裡面。之後,blockStoreShuffleFetcher 將獲取 FileSegment 資料的任務交給 basicBlockFetcherIterator。

rdd.iterator()
=> rdd(e.g., ShuffledRDD/CoGroupedRDD).compute()
=> SparkEnv.get.shuffleFetcher.fetch(shuffledId, split.index, context, ser)
=> blockStoreShuffleFetcher.fetch(shuffleId, reduceId, context, serializer)
=> statuses = MapOutputTrackerWorker.getServerStatuses(shuffleId, reduceId)

=> blocksByAddress: Seq[(BlockManagerId, Seq[(BlockId, Long)])] = compute(statuses)
=> basicBlockFetcherIterator = blockManager.getMultiple(blocksByAddress, serializer)
=> itr = basicBlockFetcherIterator.flatMap(unpackBlock)
複製程式碼
blocksByAddress

basicBlockFetcherIterator 收到獲取資料的任務後,會生成一個個 fetchRequest,每個 fetchRequest 包含去某個節點獲取若干個 FileSegments 的任務。圖中展示了 reducer-2 需要從三個 worker node 上獲取所需的白色 FileSegment (FS)。總的資料獲取任務由 blocksByAddress 表示,要從第一個 node 獲取 4 個,從第二個 node 獲取 3 個,從第三個 node 獲取 4 個。

為了加快任務獲取過程,顯然要將總任務劃分為子任務(fetchRequest),然後為每個任務分配一個執行緒去 fetch。Spark 為每個 reducer 啟動 5 個並行 fetch 的執行緒(Hadoop 也是預設啟動 5 個)。由於 fetch 來的資料會先被放到記憶體作緩衝,因此一次 fetch 的資料不能太多,Spark 設定不能超過 spark.reducer.maxSizeInFlight=48MB注意這 48MB 的空間是由這 5 個 fetch 執行緒共享的,因此在劃分子任務時,儘量使得 fetchRequest 不超過48MB / 5 = 9.6MB。如圖在 node 1 中,Size(FS0-2) + Size(FS1-2) < 9.6MB 但是 Size(FS0-2) + Size(FS1-2) + Size(FS2-2) > 9.6MB,因此要在 t1-r2 和 t2-r2 處斷開,所以圖中有兩個 fetchRequest 都是要去 node 1 fetch。那麼會不會有 fetchRequest 超過 9.6MB?當然會有,如果某個 FileSegment 特別大,仍然需要一次性將這個 FileSegment fetch 過來。另外,如果 reducer 需要的某些 FileSegment 就在本節點上,那麼直接進行 local read。最後,將 fetch 來的 FileSegment 進行 deserialize,將裡面的 records 以 iterator 的形式提供給 rdd.compute(),整個 shuffle read 結束。

//Spark shuffle read for spark 2.x
ShuffledRDD.compute()
    BlockStoreShuffleReader.read()   //fetch資料
        // 通過訊息傳送獲取 ShuffleMapTask 儲存資料位置的後設資料
=>       mapOutputTracker.getMapSizesByExecutorId(shuffleId, startPartition, endPartition)
            val statuses = getStatuses(shuffleId)  // 得到後設資料Array[MapStatus]
            	// 從driver的MapOutputTrackerMasterEndpoint遠端獲取,實際上是另起執行緒,通過LinkedBlockingQueue傳送訊息
            	val fetchedBytes = askTracker[Array[Byte]](GetMapOutputStatuses(shuffleId))
            	// 返回格式為:Seq[BlockManagerId,Seq[(shuffle block id, shuffle block size)]]
            MapOutputTracker.convertMapStatuses(shuffleId, startPartition, endPartition, statuses)
         //設定每次傳輸的大小
         SparkEnv.get.conf.getSizeAsMb("spark.reducer.maxSizeInFlight", "48m") * 1024 * 1024
         //最大遠端請求抓取block次數
         SparkEnv.get.conf.getInt("spark.reducer.maxReqsInFlight", Int.MaxValue)
=>       new ShuffleBlockFetcherIterator().initialize() 
            splitLocalRemoteBlocks()//劃分本地和遠端的blocks
                val targetRequestSize = math.max(maxBytesInFlight / 5, 1L)//每批次請求的最大位元組數,執行5個請求並行
                address.executorId != blockManager.blockManagerId.executorId //若 executorId 與本 blockManagerId.executorId不同,則從遠端獲取
                //集合多個block,達到targetRequestSize批次大小才構建一次請求
                remoteRequests += new FetchRequest(address, curBlocks)
            fetchUpToMaxBytes()//傳送遠端請求獲取blocks
                sendRequest(fetchRequests.dequeue())    //一個個傳送請求
                    //請求資料太大,會寫入磁碟shuffleFiles,否則不寫入
=>                  shuffleClient.fetchBlocks(blockIds.toArray)
                        new OneForOneBlockFetcher().start() //傳送fetch請求
            fetchLocalBlocks()// 獲取本地的Blocks
                foreach block {blockManager.getBlockData(blockId)}
                    //如果是shuffle block.則獲取經過shuffle的bolck
=>                  ExternalShuffleBlockResolver.getSortBasedShuffleBlockData()
                        //new file()的方式讀取索引檔案
                        File indexFile = getFile("shuffle_" + shuffleId + "_" + mapId + "_0.index")
                        File data = getFile("shuffle_" + shuffleId + "_" + mapId + "_0.data")
                        //FileSegmentManagedBuffer(data,offset,length)//從indexFile中獲取檔案offset和length,這裡只要獲取檔案中的一部分資料
                            //通過管道流channel讀取指定長度位元組檔案
                    //如果不是shuffle block,則從記憶體或者磁碟中直接讀取
                    diskStore.getBytes(blockId)
                        //還是通過管道流的方式讀取檔案的指定位元組內容
                        new FileInputStream(diskManager.getFile(new file(blockId.name))).getChannel()
                    memoryStore.getValues(blockId)
                        entries.get(blockId) //直接從LinkedHashMap[BlockId, MemoryEntry[_]]獲取
         
         // 若dep已進行Map端合併,就直接用mergeCombiners替代mergeValue(已經mergeValue過了)
=>       aggregator.combineCombinersByKey(combinedKeyValuesIterator)
            new ExternalAppendOnlyMap[K, C, C](identity, mergeCombiners, mergeCombiners).insertAll(iter)
                //估算集合大小,遍歷mapEntry,若記憶體不足就放磁碟
                maybeSpill(currentMap, estimatedSize)
                //直接在map上運算,update是個閉包,包含了mergeValue,createCombiner函式
                currentMap.changeValue(curEntry._1, update)
        // 若dep未進行Map端合併,還是需要對單個的vaule合併的
=>       aggregator.combineValuesByKey(keyValuesIterator) 
            new ExternalAppendOnlyMap[K, V, C](createCombiner, mergeValue, mergeCombiners).insertAll(iter)
        //對資料進行排序並寫入記憶體緩衝區,若排序中計算結果超出閾值,則將其溢寫到磁碟資料檔案
=>       new ExternalSorter().insertAll(aggregatedIter) //如果需要排序keyOrdering
            //map對應需要shouldCombine(aggregator!=none),buffer是個key和value連續放的陣列
            if (shouldCombine)  foreach record {map.changeValue((getPartition(key), update)}
            else foreach record{ buffer.insert(getPartition(key))
            maybeSpillCollection()  //超過閾值時寫入磁碟
                maybeSpill(collection, estimateSize()) //使用取樣方式估算出來的大小
                    spill(collection)//將記憶體中的集合spill到一個有序檔案中
                        val spillFile = spillMemoryIteratorToDisk(inMemoryIterator)
                            //以批次將記錄刷入檔案
                        spills += spillFile //spill出來的檔案集合
        
        //sorter.iterator觸發了排序邏輯
        CompletionIterator[Product2[K, C], Iterator[Product2[K, C]]](sorter.iterator, sorter.stop())    
             ExternalSorter.merge() //合併一組排序檔案,未寫入,返回(partition,Iterator)
                    (0 until numPartitions).iterator.map { p =>
                         (p, mergeWithAggregation(mergeCombiners))  //需要聚合時,內部還是mergeSort
                         (p, mergeSort(ordering.get))    //需要排序時
                         (p, iterators.iterator.flatten)   //都不需要,直接返回
複製程式碼

下面再討論一些細節問題:

reducer 如何將 fetchRequest 資訊傳送到目標節點?目標節點如何處理 fetchRequest 資訊,如何讀取 FileSegment 並回送給 reducer?

這部分spark 2中有改動,需要再對比

fetchrequest

rdd.iterator() 碰到 ShuffleDependency 時會呼叫 BasicBlockFetcherIterator 去獲取 FileSegments。BasicBlockFetcherIterator 使用 blockManager 中的 connectionManager 將 fetchRequest 傳送給其他節點的 connectionManager。connectionManager 之間使用 NIO 模式通訊。其他節點,比如 worker node 2 上的 connectionManager 收到訊息後,會交給 blockManagerWorker 處理,blockManagerWorker 使用 blockManager 中的 diskStore 去本地磁碟上讀取 fetchRequest 要求的 FileSegments,然後仍然通過 connectionManager 將 FileSegments 傳送回去。如果使用了 FileConsolidation,diskStore 還需要 shuffleBlockManager 來提供 blockId 所在的具體位置。如果 FileSegment 不超過 spark.storage.memoryMapThreshold=8KB ,那麼 diskStore 在讀取 FileSegment 的時候會直接將 FileSegment 放到記憶體中,否則,會使用 RandomAccessFile 中 FileChannel 的記憶體對映方法來讀取 FileSegment(這樣可以將大的 FileSegment 載入到記憶體)。

當 BasicBlockFetcherIterator 收到其他節點返回的 serialized FileSegments 後會將其放到 fetchResults: Queue 裡面,並進行 deserialization,所以 **fetchResults: Queue 就相當於在 Shuffle details 那一章提到的 softBuffer。**如果 BasicBlockFetcherIterator 所需的某些 FileSegments 就在本地,會通過 diskStore 直接從本地檔案讀取,並放到 fetchResults 裡面。最後 reducer 一邊從 FileSegment 中邊讀取 records 一邊處理。

After the blockManager receives the fetch request

=> connectionManager.receiveMessage(bufferMessage)
=> handleMessage(connectionManagerId, message, connection)

// invoke blockManagerWorker to read the block (FileSegment)
=> blockManagerWorker.onBlockMessageReceive()
=> blockManagerWorker.processBlockMessage(blockMessage)
=> buffer = blockManager.getLocalBytes(blockId)
=> buffer = diskStore.getBytes(blockId)
=> fileSegment = diskManager.getBlockLocation(blockId)
=> shuffleManager.getBlockLocation()
=> if(fileSegment < minMemoryMapBytes)
     buffer = ByteBuffer.allocate(fileSegment)
   else
     channel.map(MapMode.READ_ONLY, segment.offset, segment.length)
複製程式碼

每個 reducer 都持有一個 BasicBlockFetcherIterator,一個 BasicBlockFetcherIterator 理論上可以持有 48MB 的 fetchResults。每當 fetchResults 中有一個 FileSegment 被讀取完,就會一下子去 fetch 很多個 FileSegment,直到 48MB 被填滿。

BasicBlockFetcherIterator.next()
=> result = results.task()
=> while (!fetchRequests.isEmpty &&
        (bytesInFlight == 0 || bytesInFlight + fetchRequests.front.size <= maxBytesInFlight)) {
        sendRequest(fetchRequests.dequeue())
      }
=> result.deserialize()
複製程式碼

Discussion

這一章寫了三天,也是我這個月來心情最不好的幾天。Anyway,繼續總結。

架構部分其實沒有什麼好說的,就是設計時儘量功能獨立,模組獨立,鬆耦合。BlockManager 設計的不錯,就是管的東西太多(資料塊、記憶體、磁碟、通訊)。

這一章主要探討了系統中各個模組是怎麼協同來完成 job 的生成、提交、執行、結果收集、結果計算以及 shuffle 的。貼了很多程式碼,也畫了很多圖,雖然細節很多,但遠沒有達到原始碼的細緻程度。如果有地方不明白的,請根據描述閱讀一下原始碼吧。

如果想進一步瞭解 blockManager,可以參閱 Jerry Shao 寫的 Spark原始碼分析之-Storage模組