MapReduce執行流程

SQL寫手發表於2021-11-09

資料處理總流程

MapReduce計算框架體現的是一個分治的思想。及將待處理的資料分片在每個資料分片上並行執行相同邏輯的map()函式,然後將每一個資料分片的處理結果彙集到reduce()函式進行規約整理,最後輸出結果。
file
總體上來說MapReduce的處理流程從邏輯上看並不複雜。對於應用Hadoop進行資料分析的開發人員來說,只需實現map()方法和reduce()方法就能完成大部分的工作。正是因為Hadoop邏輯上和開發上都不復雜使它被廣泛的應用於各行各業。

Map階段

Map階段更為詳細的處理過程如圖所示:
file
一般情況下使用者需要處理分析的資料都在HDFS上。因此,MapReduce計算框架會是使用InputFormat(org.apache.hadoop.mapreduce)的子類將輸入資料分片(InputSplit)。分片後的資料將作為MapTask的輸入,MapTask會根據map()中的程式邏輯將資料分為K-V鍵值對。
為了更好的理解資料分片的過程和實現的邏輯,本文以InputFormat的一個子類FileInputFormat為例研究資料分片的過程。
FileInputFormat類將資料分片,然而這裡所說的分片並不是將資料物理上分成多個資料塊而是邏輯分片。
PS:並不是所有檔案都可以分片,比如gzip,snappy壓縮的檔案就無法分割 .
資料邏輯分片的核心方法是getSplits():

 public List<InputSplit> getSplits(JobContext job) throws IOException { 
    。。。。。。 
    List<InputSplit> splits = new ArrayList<InputSplit>(); 
    List<FileStatus> files = listStatus(job); 
    for (FileStatus file: files) { 
      Path path = file.getPath(); 
      long length = file.getLen(); 
      if (length != 0) { 
        BlockLocation[] blkLocations; 
        if (file instanceof LocatedFileStatus) { 
          blkLocations = ((LocatedFileStatus) file).getBlockLocations(); 
        } else { 
          FileSystem fs = path.getFileSystem(job.getConfiguration()); 
          blkLocations = fs.getFileBlockLocations(file, 0, length); 
        } 
        if (isSplitable(job, path)) { 
          long blockSize = file.getBlockSize(); 
          long splitSize = computeSplitSize(blockSize, minSize, maxSize); 
 
          long bytesRemaining = length; 
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { 
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); 
            splits.add(makeSplit(path, length-bytesRemaining, splitSize, 
                        blkLocations[blkIndex].getHosts(), 
                        blkLocations[blkIndex].getCachedHosts())); 
            bytesRemaining -= splitSize; 
          }           
          if (bytesRemaining != 0) { 
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); 
            splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, 
                       blkLocations[blkIndex].getHosts(), 
                       blkLocations[blkIndex].getCachedHosts())); 
          } 
        } else { // not splitable 
          splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(), 
                      blkLocations[0].getCachedHosts())); 
        } 
      } else {  
        //Create empty hosts array for zero length files 
        splits.add(makeSplit(path, 0, length, new String[0])); 
      } 
    } 
    。。。。。。 
    return splits; 
  } 

其流程圖如下所示:
file
getSplits()中的BlockLocation類儲存待處理檔案的資料塊資訊,它包含了資料塊所在DataNode的hostname,帶有快取副本的資料塊所在的節點的hostname,訪問資料塊所在DataNode的IP:埠號,在拓撲網路中的絕對路徑名,資料塊在整個資料檔案中的偏移量,資料塊長度,是否是壞塊。getSplits()會依據這些資訊建立一個FileSplit完成一個邏輯分片,然後將所有的邏輯分片資訊儲存到List中。List中的InputSplit包含四個內容,檔案的路徑,檔案開始的位置,檔案結束的位置,資料塊所在的host。
除了getSplits()方法另一比較重要的演算法是computeSplitSize()方法,它負責確定資料分片的大小,資料分片的大小對程式的效能會有一定的影響,最好將資料分片的大小設定的和HDFS中資料分片的大小一致。確定分片大小的演算法是:

Math.max(minSize, Math.min(maxSize, blockSize)) 
set mapred.max.split.size=256000000;2.x版本預設約是128M,我們叢集配置的是256M 
set mapred.min.split.size=10000000;2.x版本預設是約10M,我們叢集配置的是1 
blockSize 在hdfs-site.xml引數dfs.block.size中配置,我們叢集設定的是預設的是134217728=128M 

set mapred.map.tasks 對map task數量僅僅是參考的作用,我們叢集預設的是2 
對應的是set mapred.reduce.tasks,我們叢集預設的是-1 
reducer數量可能起作用的 
hive.exec.reducers.bytes.per.reducer=256000000 
hive.exec.reducers.max=1009 
min( hive.exec.reducers.max ,總輸入資料量/hive.exec.reducers.bytes.per.reducer) 

其中,minSize是配置檔案中設定的分片最小值,minSize則為最大值,blockSize為HDFS中資料塊的大小。
完成邏輯分片後,FileInputFormat的各個子類向MapTask對映k-v鍵值對(如TextInputFormat)。FileInputFormat的子類是對資料分片中的資料進行處理。
file
TextInputFormat中createRecorderReader()將InputSplit解析為k-v傳給mapTask,該方法中用到了LineRecordReader它繼承自RecordReader。
file
MapTask最終是通過呼叫nextKeyValue()方法來遍歷分片中的資料並且將行數以及每一行的的資料分別作為key和value傳遞給map()方法。map()方法按照開發工程師編寫的邏輯對輸入的key和value進行處理後會組成新的k-v對然後寫出到一個記憶體緩衝區中。
每個MapTask都有一個記憶體緩衝區,對緩衝區讀寫是典型的生產者消費者模式。這裡記憶體緩衝區的結構設計對MapTask的IO效率有著直接的影響。Hadoop採用了環形記憶體緩衝區,當緩衝區資料量達到閾值消費者執行緒SpillThread開始將資料寫出,於此同時充當生產者的writer()函式依然可以將處理完的資料寫入到緩衝區中。生產者和消費者之間的同步是通過可重入互斥鎖spillLock來完成的。
在寫磁碟之前,執行緒會對緩衝區內的資料進行分割槽,以決定各個資料會傳輸到哪個Reduce中。而在每個分割槽中會按key進行排序(如果此時有個Combiner則它會在排序後的輸出上執行一次,以壓縮傳輸的資料)

mapred-site.xml 檔案中 
mapreduce.task.io .sort.mb=300M 
mapreduce.map.sort.spill.percent 配置的預設只0.8 

file
使用者可以通過繼承Partitiner類並且實現getPartitioner()方法,從而定製自己的分割槽規則。預設的分割槽規則是通過key的hashCode來完成分割槽的。
環形緩衝區在達到溢寫的閾值後,溢寫到磁碟(每次溢寫都會新建一個溢寫檔案)最後合併溢寫檔案,形成一個分割槽有序的中間結果。另外可以對中間結果進行壓縮,以減少傳輸的資料量。

Reduce階段

Reduce階段更為詳細的流程如下圖所示:
file
ReduceTask對資料進行規約的第一步就是從MapTask的輸出磁碟上將資料拉取過來。這個過程重點分析shuffle類和Fetcher類。Shuffle類如下圖所示:
file
Shuffle類中的init()方法負責初始化Shuffle階段需要的上下文,並且在Shuffle的最後階段呼叫歸併排序方法。Shuffle類的核心方法為run()方法。

public RawKeyValueIterator run() throws IOException, InterruptedException {
	。。。。。。 
	// Start the map-output fetcher threads 
	Boolean isLocal = localMapFiles != null;
	final int numFetchers = isLocal ? 1 : 
	jobConf.getint(MRJobConfig.SHUFFLE_PARALLEL_COPIES, 5);
	Fetcher<K,V>[] fetchers = new Fetcher[numFetchers];
	if (isLocal) {
		fetchers[0] = new LocalFetcher<K, V>(jobConf, reduceId, scheduler, 
		merger, reporter, metrics, this, reduceTask.getShuffleSecret(), 
		localMapFiles);
		fetchers[0].start();
	} else {
		for (int i=0; i < numFetchers; ++i) {
			fetchers[i] = new Fetcher<K,V>(jobConf, reduceId, scheduler, merger,  
			reporter, metrics, this,reduceTask.getShuffleSecret());
			fetchers[i].start();
		}
	}
	。。。。。。 
	eventFetcher.shutDown();
	for (Fetcher<K,V> fetcher : fetchers) {
		fetcher.shutDown();
	}
	scheduler.close();
	copyPhase.complete();
	// copy is already complete 
	taskStatus.setPhase(TaskStatus.Phase.SORT);
	reduceTask.statusUpdate(umbilical);
	RawKeyValueIterator kvIter = null;
	。。。。。。 
	return kvIter;
}

在run()方法中它是通過啟動fetcher執行緒來拉取資料的。首先需要判斷將要拉取的資料是否具有本地性,如果資料在本地則直接傳入檔案的地址否則建立fetcher執行緒來從其他節點遠端拉取資料。Fetcher類類圖如下:
file
Fetcher繼承自Thread類因此它重寫了run()方法並且呼叫了copyFromHost()方法。copyFromHost()方法首先獲取指定host上執行完成的MapTaskID然後迴圈的從Map段讀取資料直到所有的資料都讀取完成。

 protected void copyFromHost(MapHost host) throws IOException {
	。。。。。。 
	List<TaskAttemptID> maps = scheduler.getMapsForHost(host);
	。。。。。。 
	while (!remaining.isEmpty() && failedTasks == null) {
		try {
			failedTasks = copyMapOutput(host, input, remaining, fetchRetryEnabled);
		}
		catch (IOException e) {
			。。。。。
		}
	}
}

讀取資料是在copyMapOutput()方法中完成的,方法中用到了ShufferHeader類它實現了Writable介面從而可以完成序列化與反序列化的工作,它呼叫readFields()方法從資料流中讀取資料。
file

mapreduce.task.io .sort.factor =25 

讀取資料過程中需要注意的是,如果中間結果小則複製到記憶體緩衝區中否則複製到本地磁碟中。當記憶體緩衝區達到大小閾值或者檔案數閾值則溢寫到本地磁碟,與此同時後臺執行緒會不停的合併溢寫檔案形成大的有序的檔案。
在Shuffle-copy階段進行的同時Shuffle-Sort也在處理資料,這個階段就是針對記憶體中的資料和磁碟上的資料進行歸併排序。
複製完所有的map輸出做迴圈歸併排序合併資料。舉個例子更加好理解,若合併因子為10,50個輸出檔案,則合併5次,最後剩下5個檔案不符合合併條件,則將這5個檔案交給Reduce處理。
Reduce階段會接收到已經排完序的k-v對,然後對k-v對進行邏輯處理最後輸出結果k-v對到HDFS中.

相關文章