注意:
- 通過WordCount程序爲例進行測試
- 是在本地模式進行的,所以N個MapTask 和 N個 ReduceTask沒有並行的效果。
- 如果在集羣上,N個 MapTask 和 N 個ReduceTask 是並行運行.
一、 Job提交的流程
方法層級:1 > 1) > (1) > <1> > ① > [1] > {1}
1. job.waitForCompletion(true); //在Driver中提交job
1)sumbit() //提交
(1)connect():
<1>return new Cluster(getConfiguration());
① initialize(jobTrackAddr, conf);
//通過YarnClientProtocolProvider或LocalClientProtocolProvider
//根據配置文件的參數信息獲取當前job需要執行到本地還是Yarn
//最終:LocalClientProtocolProvider ==> LocalJobRunner
(2) return submitter.submitJobInternal(Job.this, cluster); //提交job
<1> .checkSpecs(job);// 檢查job的輸出路徑。
<2> . Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
//生成Job提交的臨時目錄:
//D:\tmp\hadoop\mapred\staging\Administrator1777320722\.staging
<3> . JobID jobId = submitClient.getNewJobID(); //爲當前Job生成Id
<4> . Path submitJobDir = new Path(jobStagingArea, jobId.toString()); //Job的提交路徑
d:/tmp/hadoop/mapred/staging/Administrator1777320722/.staging/job_local1777320722_0001
<5> . copyAndConfigureFiles(job, submitJobDir);
① rUploader.uploadResources(job, jobSubmitDir);
[1] uploadResourcesInternal(job, submitJobDir);
{1}.submitJobDir = jtFs.makeQualified(submitJobDir);
mkdirs(jtFs, submitJobDir, mapredSysPerms);//創建Job的提交路徑
<6> . int maps = writeSplits(job, submitJobDir); //生成切片信息 ,並返回切片的個數
<7> . conf.setInt(MRJobConfig.NUM_MAPS, maps); //通過切片的個數設置MapTask的個數
<8> . writeConf(conf, submitJobFile); //將當前Job相關的配置信息寫到job提交路徑下
//此時通過查看發現路徑下有: job.split job.splitmetainfo job.xml xxx.jar
<9> .status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());//此時才真正提交Job
<10> . jtFs.delete(submitJobDir, true); //等job執行完成後,刪除Job的臨時工作目錄的內容
二、 MapTask的工作機制
1. 從Job提交流程代碼解析裏面的的(2)--><9> 進去
Job job = new Job(JobID.downgrade(jobid), jobSubmitDir); //構造真正執行的Job , LocalJobRunnber$Job
2. LocalJobRunnber$Job 的run()方法
1)TaskSplitMetaInfo[] taskSplitMetaInfos = SplitMetaInfoReader.readSplitMetaInfo(jobId, localFs, conf, systemJobDir);// 讀取job.splitmetainfo
2)int numReduceTasks = job.getNumReduceTasks(); // 獲取ReduceTask個數
3) // 根據切片的個數, 創建執行MapTask的 MapTaskRunnable
List<RunnableWithThrowable> mapRunnables = getMapTaskRunnables(taskSplitMetaInfos, jobId, mapOutputFiles);
4)ExecutorService mapService = createMapExecutor(); // 創建線程池
5) runTasks(mapRunnables, mapService, "map"); //執行 MapTaskRunnable
6) 因爲Runnable提交給線程池執行,接下來會執行MapTaskRunnable的run方法。
7) 執行 LocalJobRunner$Job$MapTaskRunnable 的run()方法.
(1) MapTask map = new MapTask(systemJobFile.toString(), mapId, taskId,info.getSplitIndex(), 1); //創建MapTask對象
(2) map.run(localConf, Job.this); //執行MapTask中的run方法
<1> .runNewMapper(job, splitMetaInfo, umbilical, reporter);
① org.apache.hadoop.mapreduce.TaskAttemptContext taskContext = JobContextImpl
② org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper = WordConutMapper
③ org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat = TextInputFormat
④ split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset();
//重構切片對象
//切片對象的信息 : file:/D:/input/inputWord/JaneEyre.txt:0+36306679
⑤org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input = MapTask$NetTrackingRecordReader
⑥output = new NewOutputCollector(taskContext, job, umbilical, reporter); //構造緩衝區對象
[1] collector = createSortingCollector(job, reporter); //獲取緩衝區對象
MapTask$MapOutputBuffer
{1} . collector.init(context); //初始化緩衝區對象
1>>.final float spillper = job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);// 默認溢寫百分比 0.8
2>>.final int sortmb = job.getInt(MRJobConfig.IO_SORT_MB,MRJobConfig.DEFAULT_IO_SORT_MB); // 默認緩衝區大小 100M
3>>.sorter = ReflectionUtils.newInstance(job.getClass(MRJobConfig.MAP_SORT_CLASS, QuickSort.class,IndexedSorter.class), job);
// 排序對象
// 排序使用的是快排,並且只是基於索引排序。
4>> . // k/v serialization // kv序列化
5>> . // output counters // 計數器
6>> . // compression // 壓縮
7>> . // combiner // combiner
⑦ mapper.run(mapperContext);// 執行WordCountMapper中的run方法。 實際執行的是WordCountMapper繼承的Mapper中的run方法。
[1] . 在Mapper中的run方法中 :
map(context.getCurrentKey(), context.getCurrentValue(), context);//執行到WordCountMapper中的map方法。
[2] . 在WordCountMapper中的map方法中將kv寫出 :
context.write(outK,outV);
三、 Shuffle流程(溢寫,歸併)
1. map中的kv持續往 緩衝區寫, 會達到溢寫條件,發生溢寫,最後發生歸併。
2. map中的 context.write(k,v)
1) . mapContext.write(key, value);
(1). output.write(key, value);
<1> collector.collect(key, value,partitioner.getPartition(key, value, partitions));
// 將map寫出的kv 計算好分區後,收集到緩衝區中。
<2> . 當滿足溢寫條件後 ,開始發生溢寫
startSpill();
① spillReady.signal(); //線程間通信,通知溢寫線程開始溢寫
② 溢寫線程調用 sortAndSpill() 方法發生溢寫操作
③ final SpillRecord spillRec = new SpillRecord(partitions);
final Path filename = mapOutputFile.getSpillFileForWrite(numSpills, size);
out = rfs.create(filename)
//根據分區的個數,創建溢寫文件:
/tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local277309786_0001/attempt_local277309786_0001_m_000000_0/output/spill0.out
④ sorter.sort(MapOutputBuffer.this, mstart, mend, reporter); // 溢寫前先排序
⑤ writer.close(); 通過writer進行溢寫,溢寫完成後,關閉流,可以查看磁盤中的溢寫文件
⑥ if (totalIndexCacheMemory >= indexCacheMemoryLimit)
// create spill index file
Path indexFilename =mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions
// 判斷索引使用的內存空間是否超過限制的大小,如果超過也需要溢寫到磁盤
⑦ map持續往緩衝區寫,達到溢寫條件,就繼續溢寫 ........ 可能整個過程中發生N次溢寫。
⑧ MapTask中的runNewMapper 中 output.close(mapperContext);
假如上一次溢寫完後,剩餘進入的到緩衝區的數據沒有達到溢寫條件,那麼當map中的所有的數據
都已經處理完後,在關閉output時,會把緩衝區中的數據刷到磁盤中(其實就是沒有達到溢寫條件的數據也要寫到磁盤)
[1] collector.flush(); //刷寫
{1} . sortAndSpill(); 通過溢寫的方法進行剩餘數據的刷寫
{2} . 最後一次刷寫完後,磁盤中會有N個溢寫文件
spill0.out spill1.out .... spillN.out
{3} . 歸併 mergeParts();
>>1. for(int i = 0; i < numSpills; i++) {
filename[i] = mapOutputFile.getSpillFile(i);
finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();
}
//根據溢寫的次數,得到要歸併多少個溢寫文件
>>2. Path finalOutputFile = mapOutputFile.getOutputFileForWrite(finalOutFileSize);
/tmp/hadoopAdministrator/mapred/local/localRunner/Administrator/jobcache/job_local1987086776_0001/attempt_local1987086776_0001_m_000000_0/output/file.out
Path finalIndexFile = mapOutputFile.getOutputIndexFileForWrite(finalIndexFileSize);
/tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local1987086776_0001/attempt_local1987086776_0001_m_000000_0/output/file.out.index
//生成最終存儲數據的兩個文件
>>3. for (int parts = 0; parts < partitions; parts++) {
// 按照分區的, 進行歸併。
>>4. awKeyValueIterator kvIter = Merger.merge(job, rfs,
keyClass, valClass, codec,
segmentList, mergeFactor,
new Path(mapId.toString()),
job.getOutputKeyComparator(), reporter, sortSegments,
null, spilledRecordsCounter, sortPhase.phase(),
TaskType.MAP);
//歸併操作
>>5 Writer<K, V> writer = new Writer<K, V>(job, finalPartitionOut, keyClass, valClass, codec,spilledRecordsCounter);
//通過writer寫歸併後的數據到磁盤
>>6 .
if (combinerRunner == null || numSpills < minSpillsForCombine) {
Merger.writeFile(kvIter, writer, reporter, job);
} else {
combineCollector.setWriter(writer);
combinerRunner.combine(kvIter, combineCollector);
}
在歸併時,如果有combine,且溢寫的次數大於等於minSpillsForCombine的值3纔會使用Combine
>>7.
for(int i = 0; i < numSpills; i++) {
rfs.delete(filename[i],true);
}
歸併完後,將溢寫的文件刪除
>> 8. 最後在磁盤中存儲map處理完後的數據,等待reduce的拷貝。
file.out file.out.index
四、 ReduceTask工作機制
1. 在LocalJobRunner$Job中的run()方法中
if (numReduceTasks > 0) {
//根據reduceTask的個數,創建對應個數的LocalJobRunner$Job$ReduceTaskRunnable
List<RunnableWithThrowable> reduceRunnables = getReduceTaskRunnables(
jobId, mapOutputFiles);
//線程池
ExecutorService reduceService = createReduceExecutor();
//將 ReduceTaskRunnable提交給線程池執行
runTasks(reduceRunnables, reduceService, "reduce");
}
1) . 執行LocalJobRunner$Job$ReduceTaskRunnable 中的run方法
(1) . ReduceTask reduce = new ReduceTask(systemJobFile.toString(),reduceId, taskId, mapIds.size(), 1);
//創建ReduceTask對象
(2) . reduce.run(localConf, Job.this); // 執行ReduceTask的run方法
<1> . runNewReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
[1] . org.apache.hadoop.mapreduce.TaskAttemptContext taskContext = TaskAttemptContextImpl
[2] . org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE> reducer = WordCountReducer
[3] . org.apache.hadoop.mapreduce.RecordWriter<OUTKEY,OUTVALUE> trackedRW = ReduceTask$NewTrackingRecordWriter
[4] . reducer.run(reducerContext);
//執行WordCountReducer的run方法 ,實際執行的是WordCountReducer繼承的Reducer類中的run方法.
{1} .reduce(context.getCurrentKey(), context.getValues(), context);
//執行到WordCountReducer中的 reduce方法.
{2} . context.write(k,v) 將處理完的kv寫出.
>>1 . reduceContext.write(key, value);
>>2 . output.write(key, value);
>>3 . real.write(key,value); // 通過RecordWriter將kv寫出
>>4 . out.write(NEWLINE); //通過輸出流將數據寫到結果文件中