详细分析Job、MapTask、Shuffle、ReduceTask工作流程,全过程源码深度解析
文章目录
MapTask工作机制
(1)Read阶段:MapTask通过InputFormat
获得的RecordReader
,从输入InputSplit中解析出一个个key/value。
(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
(4)Spill阶段:即"溢写",当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次快速排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
-
步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号
Partition
进行排序,然后按照key
进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序。 -
步骤2:按照分区编号由小到大,依次将每个分区中的数据,写入任务工作目录下的临时文
output/spillN.out(N表示当前溢写次数)
中。如果用户设置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。 -
步骤3:将分区数据的元信息,写到内存索引数据结构
SpillRecord
中,其中每个分区的元信息包括,在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index
中。
(5)Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor
(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
ReduceTask工作机制
(1)Copy阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(4)Reduce阶段:context.write(k,v)方法将结果写出。
Job提交流程源码解析
一、 Job提交流程源码解析
一、 Job提交流程源码解析
1. job.waitForCompletion(true); // 在Driver中提交job
1) sumbit() 提交
(1) connect(): // 就是获取JobRunner对象(yarn或local)
<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的输出路径。
// 生成Job提交的临时目录 D:\tmp\hadoop\mapred\staging\Administrator1777320722\.staging
<2> Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 为当前Job生成Id
<3> JobID jobId = submitClient.getNewJobID();
// 拼接Job的提交目录和jobID
// d:/tmp/hadoop/mapred/staging/Administrator1777320722/.staging/job_local1777320722_0001
<4> Path submitJobDir = new Path(jobStagingArea, jobId.toString());
<5> copyAndConfigureFiles(job, submitJobDir);
① rUploader.uploadResources(job, jobSubmitDir);
[1] uploadResourcesInternal(job, submitJobDir);
{1} submitJobDir = jtFs.makeQualified(submitJobDir);
// 真正创建Job的提交路径
mkdirs(jtFs, submitJobDir, mapredSysPerms);
// 生成切片信息 ,并返回切片的个数
<6> int maps = writeSplits(job, submitJobDir);
// 通过切片的个数设置MapTask的个数
<7> conf.setInt(MRJobConfig.NUM_MAPS, maps);
// 将当前Job相关的配置信息写到job提交路径下
// 路径下: job.split job.splitmetainfo job.xml xxx.jar
<8> writeConf(conf, submitJobFile);
//真正提交Job
<9> status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
// 等job执行完成后,删除Job的临时工作目录
<10> jtFs.delete(submitJobDir, true);
MapTask工作流程源码解析
二、 MapTask工作流程源码解析
二、 MapTask工作流程源码解析
1. 从Job提交流程的(2)--><9> 进去
// 构造真正执行的Job , LocalJobRunnber$Job
Job job = new Job(JobID.downgrade(jobid), jobSubmitDir);
2. LocalJobRunnber$Job 的run()方法
// 读取job.splitmetainfo 切片的元信息
1) TaskSplitMetaInfo[] taskSplitMetaInfos = SplitMetaInfoReader.readSplitMetaInfo(jobId, localFs, conf, systemJobDir);
// 获取ReduceTask个数
2) int numReduceTasks = job.getNumReduceTasks();
// 根据切片的个数,创建执行MapTask的 MapTaskRunnable
3) List<RunnableWithThrowable> mapRunnables = getMapTaskRunnables(taskSplitMetaInfos, jobId, mapOutputFiles);
// 创建线程池
4) ExecutorService mapService = createMapExecutor();
//执行 MapTaskRunnable
5) runTasks(mapRunnables, mapService, "map");
6) 因为Runnable提交给线程池执行,接下来会执行MapTaskRunnable的run方法。
7) 执行 LocalJobRunner$Job$MapTaskRunnable 的run()方法.
//创建MapTask对象(普通类),每个MapTaskRunnable线程里都会创建MapTask对象
(1) MapTask map = new MapTask(systemJobFile.toString(), mapId, taskId,info.getSplitIndex(), 1);
//执行MapTask中的run方法,(是MapTask类的,普通方法)
(2) map.run(localConf, Job.this);
<1> runNewMapper(job, splitMetaInfo, umbilical, reporter);
// 获取taskContext,job的上下文环境
① org.apache.hadoop.mapreduce.TaskAttemptContext taskContext = JobContextImpl
// 获取我们自定义Mapper(WordConutMapper)
② org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper = WordConutMapper
// 获取InputFormat实现类对象(TextInputFormat)
③ 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
// 读数据的RecordReader
⑤ 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
// 执行WordCountMapper中的run方法。 实际执行的是WordCountMapper继承的Mapper中的run方法。
⑦ mapper.run(mapperContext);
{1} 在Mapper中的run方法中,循环执行map(context.getCurrentKey(), context.getCurrentValue(), context); 实际上执行的是WordCountMapper中的map方法。
{2} 在WordCountMapper中的map方法中将kv写出context.write(outK,outV);
Shuffle流程(溢写,归并)源码解析
三、 Shuffle流程(溢写,归并)源码解析
三、 Shuffle流程(溢写,归并)源码解析
1. map中的kv持续往 缓冲区写,会达到溢写条件,发生溢写,最后发生归并。
2. map中的 context.write(k,v)
1) mapContext.write(key, value);
(1) output.write(key, value);
// 将map写出的kv 计算好分区后,收集到缓冲区中。
<1> collector.collect(key, value, partitioner.getPartition(key, value, partitions));
// 当满足溢写条件后,开始发生溢写
<2> startSpill();
// 线程间通信,通知溢写线程开始溢写
① spillReady.signal();
// 溢写线程调用sortAndSpill()方法,执行溢写操作
② sortAndSpill()
// 获取分区号
③ final SpillRecord spillRec = new SpillRecord(partitions);
// 根据分区的个数,创建溢写文件,
// /tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local277309786_0001/attempt_local277309786_0001_m_000000_0/output/spill0.out
final Path filename = mapOutputFile.getSpillFileForWrite(numSpills, size);
out = rfs.create(filename)
// 溢写前先排序
④ sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
// 通过writer进行溢写,溢写完成后,关闭流,可以查看磁盘中的溢写文件
⑤ writer.close();
// 判断索引使用的内存空间 是否超过 限制的大小,如果超过也需要溢写到磁盘
⑥ if (totalIndexCacheMemory >= indexCacheMemoryLimit)
// create spill index file
Path indexFilename = mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions)
⑧ map持续往缓冲区写,达到溢写条件,就继续溢写 ........ 可能整个过程中发生N次溢写。
// 假如上一次溢写完后,剩余进入到缓冲区的数据,没有达到溢写条件,那么当map中的所有的数据都已经处理完后,在关闭output时,会把缓冲区中的数据刷到磁盘中(其实就是,最后没有达到溢写条件的数据,也要写到磁盘)
⑦ MapTask类中的runNewMapper()方法中的output.close(mapperContext);
[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/hadoop-Administrator/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);
//通过writer写归并后的数据到磁盘
>>5 Writer<K, V> writer = new Writer<K, V>(job, finalPartitionOut, keyClass, valClass, codec,spilledRecordsCounter);
//在归并时,如果有combine,且溢写的次数 大于等于 minSpillsForCombine的值3时,才会执行Combine
>>6 if (combinerRunner == null || numSpills < minSpillsForCombine) {
Merger.writeFile(kvIter, writer, reporter, job);
} else {
combineCollector.setWriter(writer);
combinerRunner.combine(kvIter, combineCollector);
}
// 归并完后,将溢写的文件删除
>>7 for(int i = 0; i < numSpills; i++) {
rfs.delete(filename[i],true);
}
>>8 最后在磁盘中存储map处理完后的数据,等待reduce的拷贝。
// file.out file.out.index
ReduceTask工作流程源码解析
四、ReduceTask工作流程源码解析
四、ReduceTask工作流程源码解析
1. 在LocalJobRunner$Job中的run()方法中 // 跟启动MapTask的机制类似
// 先判断,只有numReduceTasks个数 大于0,才会有ReduceTask
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方法
//创建ReduceTask对象
(1) ReduceTask reduce = new ReduceTask(systemJobFile.toString(), reduceId, taskId, mapIds.size(), 1);
(2) reduce.run(localConf, Job.this); // 执行ReduceTask的run方法
<1> runNewReducer(job, umbilical, reporter, rIter, comparator, keyClass, valueClass);
// 获取taskContext,job的上下文环境
[1] org.apache.hadoop.mapreduce.TaskAttemptContext taskContext = JobContextImpl
// 获取我们自定义Reduce(WordConutReduce)
[2] org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE> reducer = WordCountReducer
// 写数据RecordWriter
[3] org.apache.hadoop.mapreduce.RecordWriter<OUTKEY,OUTVALUE> trackedRW = ReduceTask$NewTrackingRecordWriter
//执行WordCountReducer的run方法 ,实际执行的是WordCountReducer继承的Reducer类中的run方法.
[4] reducer.run(reducerContext);
//执行到WordCountReducer中的 reduce方法.
{1} reduce(context.getCurrentKey(), context.getValues(), context);
{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); // 通过输出流将数据写到结果文件中