day45-hadoop-mapreduce
day45-hadoop-mapreduce
hadoop-mapreduce
MR原码解读
Job的提交过程
一. job.waitForCompletion(true);
1. submit(); 确认job的状态为DEFINE,进行job的提交
1.1 ensureState(JobState.DEFINE); 再次确认状态
1.2 setUseNewAPI(); 设置使用新的API,hadoop有两套API(旧:mapred,新:mapreduce)
1.3 connect(); 连接?
1.3.1 return new Cluster(getConfiguration());创建Cluster对象
1) initialize(jobTrackAddr, conf);
1.1) for (ClientProtocolProvider provider : providerList) {
根据job的信息,得出job要运行在哪里
providderList:YarnClinentProtocolProvider | LocalClientProtocolProvider
1.2) 分别通过 YarnClinentProtocolProvider | LocalClientProtocolProvider结合当前job的配置信息。来获取job运行的环境
当前Job是在本地执行,因此最终获取到 LocalJobRunner。如果将来job是在Yarn执行的话,最终获取到的是YarnRunner
1.4 return submitter.submitJobInternal(Job.this, cluster); // 提交job
1.4.1 checkSpecs(job); //验证job的输出命令
1.4.2 Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);获取job的临时工作区间
D:/tmp/hadoop/mapred/staging/Administrator299837771/.staging
1.4.3 job.setJobID(jobId); // 设置jobId
job_local299837771_0001
1.4.4 Path submitJobDir = new Path(jobStagingArea, jobId.toString()); //生成job的提交路径
file:/tmp/hadoop/mapred/staging/Administrator299837771/.staging/job_local299837771_0001
1.4.5 copyAndConfigureFiles(job, submitJobDir);
1) rUploader.uploadResources(job, jobSubmitDir); //将job的资源提交到job提交的路径
1.1) mkdirs(jtFs, submitJobDir, mapredSysPerms);// 创建job的提交路径
1.4.6 int maps = writeSplits(job, submitJobDir); // 生成切片,并返回切片的个数
job.split
job.splitmetainfo
1.4.7 conf.setInt(MRJobConfig.NUM_MAPS, maps); // 根据切片的个数设置MapTask的个数
1.4.8 writeConf(conf, submitJobFile);
job.xml
1.4.9 status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
// 真正提交job
1.4.10 jtFs.delete(submitJobDir, true); // 最后将job提交路径删除
Job提交流程的关键点:
Driver中提交job --> 明确job的运行环境(本地|yarn)--> 规划Job的提交路径 --> 生成切片并将切片信息写入到job的提交路径下 --> 将job的配置信息写入到job的提交路径下
MapTask
一 、status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());// 真正的提交job
二、 Job job = new Job(JobID.downgrade(jobid), jobSubmitDir); //创建真正执行的Job对象
1. this.start(); //创建好Job对象后,直接启动
Job对象:LocalJobRunner$Job 是一个线程
2. 接下来执行Job的run方法
2.1 TaskSplitMetaInfo[] taskSplitMetaInfos =
SplitMetaInfoReader.readSplitMetaInfo(jobId, localFs, conf, systemJobDir);
// 读取切片信息
2.2 List<RunnableWithThrowable> mapRunnables = getMapTaskRunnables(
taskSplitMetaInfos, jobId, mapOutputFiles);
//根据切片的信息,创建对应个数的MapTask执行的Runable对象
Runnable对象:LocalJobRunner$Job$MapTaskRunnable
2.3 ExecutorService reduceService = createReduceExecutor();//创建线程池
2.4 runTasks(reduceRunnables, reduceService, "reduce");
// 将所有的LocalJobRunner$Job$MapTaskRunnable对象交给线程池
2.4.1 service.submit(r);// 将每个Runnable对象交给线程池执行
2.4.2 开始执行Runnable对象的run方法: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方法
① runNewMapper(job, splitMetaInfo, umbilical, reporter);
Ⅰ创建mapper对象:wordConntCapper
Ⅱ创建InputFormat 对象:TextInputFormat
Ⅲ 重构切片对象:file:/D:/a测试文件/input/wordcount2/abc.txt:0+33554432
读file:/D:/a测试文件/input/wordcount2/abc.txt文件,从0读到33554432这个位置,逻辑切片
Ⅳ 创建RecordReader对象
Ⅴ 创建缓冲区对象 output = new NewOutputCollector(taskContext, job, umbilical, reporter);
Ⅵ mapper.run(mapperContext);// 执行map中的run方法
Ⅶ 执行WordCountMapper中的Map方法
Ⅷ context.write(outk,ouv) // 将kv写出,写到缓存区中
三、创建缓冲区对象
1. output = new NewOutputCollector(taskContext, job, umbilical, reporter);// 创建缓存区对象
1.1 collector = createSortingCollector(job, reporter);
1.1.1 collector.init(context); // 初始化缓冲区对象
collector: MapTaask$MapOutputBuffer对象
1) final float spillper =job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
// 溢写百分比,默认0.8
相关参数:mapreduce.map.sort.spill.percent
2) final int sortmb = job.getInt(MRJobConfig.IO_SORT_MB,MRJobConfig.DEFAULT_IO_SORT_MB);
//缓存取大小,默认100MB
相关参数:mapreduce.task.io.sort.mb
3) sorter = ReflectionUtils.newInstance(job.getClass(MRJobConfig.MAP_SORT_CLASS,QuickSort.class,IndexedSorter.class), job);
sorter: QuickSoter // 默认使用的是快速排序,特点只排索引
4) comparator = job.getOutputKeyComparator();// 获取排序比较器
5) combiner
6) 启动溢写线程
7) 有两个线程工作:收集线程(将kv收集到缓冲区中) 溢写线程(将kv溢写到磁盘)
1.2:获取分区器对象
partitions = jobContext.getNumReduceTasks();
if (partitions > 1) {
partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
} else {
partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
@Override
public int getPartition(K key, V value, int numPartitions) {
return partitions - 1;
}
};
}
}
四、溢写和归并
1. context.write(outk,ouv) // 在WordCountMapper中将kv持续写入到缓冲区
2. mapContext.write(key, value);-> output.write(key, value);
collector.collect(key, value,partitioner.getPartition(key, value, partitions));
// 计算好kv的分区,并收集到缓冲区中
2.1 startSpill(); // 持续将kv往缓冲区写,当满足溢写条件后,开始溢写
2.1.1 spillReady.signal(); // 通知溢写线程开始干活
2.1.2 溢写线程工作时,会调用sortAndSpill() 排序排序并溢写
1) final Path filename =mapOutputFile.getSpillFileForWrite(numSpills, size);
// 生成溢写文件
/tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local1821803294_0001/attempt_local1821803294_0001_m_000000_0/output/spill0.out
2) out = rfs.create(filename);// 创建溢写文件
3) sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);// 溢写前排序
4) for (int i = 0; i < partitions; ++i) { // 按照分区进行溢写
5) writer.close();//本次溢写结束
6) 后续数据持续往缓冲区写,如果满足80%就发生溢写,如果最后的数据没有满足80%,则再关闭MapTask端输出对象时,将缓冲区的剩余数据写道溢写文件中
7) output.close(mapperContext);// 关闭输出对象
① collector.flush()// 将缓存区的最后数据刷出去
①->sortAndSpill();//将数据排序并写到溢写文件中
目前数据已经全部都写到了磁盘中:spill0.out spill1.out
[1] 明确有多少个溢写文件
for (int i = indexCacheList.size(); i < numSpills; ++i) {
Path indexFileName = mapOutputFile.getSpillIndexFile(i);
indexCacheList.add(new SpillRecord(indexFileName, job));
}
[2] 归并后的文件、归并后的索引文件
Path finalOutputFile =mapOutputFile.getOutputFileForWrite(finalOutFileSize);
D:/tmp/hadoop-Administrator/mapred/local/localRunner
/Administrator/jobcache/job_local1821803294_0001/attempt_local1821803294_0001_m_000000_0/output/file.out
Path finalIndexFile=mapOutputFile.getOutputIndexFileForWrite(finalIndexFileSize);
D:/tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local1821803294_0001
/attempt_local1821803294_0001_m_000000_0/output/file.out.index
[3] 判断是否使用combiner
if (combinerRunner == null || numSpills < minSpillsForCombine) {
[4] 写索引文件
spillRec.writeToFile(finalIndexFile, job);
[5] 删除所有的溢写文件
for(int i = 0; i < numSpills; i++) {
rfs.delete(filename[i],true);
}
3. 总结:整个过程中,可能发生N次溢写,会有N个溢写文件,例如 spill0.out spill1.out
全部溢写结束后,会发生归并,生成最终的两个文件:
file.out
file.out.index
并将所有溢写文件删除
reduce需要到file.out 里面拷贝数据
ReduceTask
一、LocalJobRunner$Job中的run方法:
1. List<RunnableWithThrowable> reduceRunnables = getReduceTaskRunnables(jobId, mapOutputFiles);
// 创建LocalJobRunner$Job$ReduceTaskRunnable对象
2. ExecutorService reduceService = createReduceExecutor();
// 创建线程池
3. runTasks(reduceRunnables, reduceService, "reduce");
// 交给线程池执行
3.1 service.submit(r); // 将每个ReduceTaskRunnable对象提交给线程池执行
3.2 开始执行ReduceTaskRunnable的run方法
3.2.1 ReduceTask reduce = new ReduceTask(systemJobFile.toString(),
reduceId, taskId, mapIds.size(), 1);
// 创建ReduceTask对象
3.2.2 reduce.run(localConf, Job.this);// 执行ReduceTask的run方法
1) runNewReducer(job, umbilical, reporter, rIter, comparator,keyClass, valueClass);
① 创建reducer对象:WordCountReducer
② reducer.run(reducerContext); // 执行Reducer类中的run方法
[1] 执行到WordCountReducer中的reduce方法
[2] context.write(key,outv) // 将kv写到最终的文件中
reduceContext.write(key, value) --> output.write(key, value) --> real.write(key,value) --> writeObject(key)
原码总结
上述调试的原码过程是本地执行的情况,不能完全对应到集群的情况
集群:
Job提交的时候,在集群上,除了我们看到的切片信息,xml信息,还需要提交jar信息
多个MapTask是并行执行。最终每个MapTask都会生成结果文件:file.out file.out.index
多个ReduceTask是并行执行,根据自身所负责的分组,到每个MapTask中拷贝对应分区的数据
MapTask的工作机制

1. Read阶段:MapTask通过用户编写的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. Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,他将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
ReduceTask工作机制

1. Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,并针对某一片数据,如果其大小炒作一定阈值,则写到磁盘上,否则直接放到内存中。
2. Merge阶段,在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
3. Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
4. Reduce阶段:reduce()函数将计算结果写到HDFS上。
设置ReduceTask并行度
ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:
// 默认值是1,手动设置为4
job.setNumteduceTasks(4)

浙公网安备 33010602011771号