前言
通读源码这东西,是每一个优秀的程序员都应该做到的基本功,学了大数据后更着网上的视频读了几次源码了,但还是毫无头绪,每次都思维混乱、痛苦不堪,全程感觉在哲学三问。
近日有老师领着又读了一遍,感觉稍微好点了,遂自己又简单的读了一遍,总算是有点头绪了,因此写下此篇,供自己和其他学习大数据的新手们在读源码时做个参考。
环境说明
- 我所用的版本时hadoop2.7.2,jdk1.8,都安装在windows上且配好了环境。
- 使用的java编译器时idea,阅读源码主要借助于idea的断点调试功能
- 基于我自己写的wordcount程序运行
- 其中的driver类如下
/**
* 此类直接写main方法封装一些必要的信息到job中并提交即可
*/
public class WCDriver {
public static void main(String[] args)
throws IOException, ClassNotFoundException, InterruptedException {
//设置输入输出路径
Path inputPath = new Path(
"E:/work/test/input/inputfile.txt");
Path outputPath = new Path(
"E:/work/test/output");
//如果输出路径已经存在会抛出异常,
//所以判断输出路径是否存在,如果存在则将其删除
FileSystem fs = FileSystem.get(new Configuration());
if (fs.exists(outputPath)){
fs.delete(outputPath,true);
}
//创建job
Job job = Job.getInstance();
//设置job
//设置要运行的mapper和reducer类
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//设置mapper和reducer的输出k-v类型,
// 如果两者一致,直接设置最终输出类型即可
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置输入目录和输出目录
FileInputFormat.setInputPaths(job,inputPath);
FileOutputFormat.setOutputPath(job, outputPath);
//开启并等待任务执行完成
boolean result = job.waitForCompletion(true);
System.out.println(result?1:-1);
}
}
通过idea的断点调试粗读MapReduce的java源码
-
在WCDriver中为最后的job任务提交处断点,step into
//此句断点,MapReduce程序正式开始 boolean result = job.waitForCompletion(true);
-
进入该方法后,我们不难发现该方法是对submit方法的封装,我们同样可以直接通过调用submit方法提交job任务,不过该封装方法提供了一些条件判断和错误处理,在submit处断点,step into
if (state == JobState.DEFINE) {
//此句断点 step into
submit();
}
-
进入submit方法后,我们不难通过名字推测当先几行代码的功能分别是确认状态、设置使用新的API、连接、创建提交器对象:(对此类不是我们所关心的mapreduce核心流程代码,此后都略过不表,自己读源码时可以简单浏览,如果不懂可以跳过)
ensureState(JobState.DEFINE); setUseNewAPI(); connect(); final JobSubmitter submitter = getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
-
我们关注的应该是这一行,见名知意,可以推测其是内部提交job的方法,打断点,step into:
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() { public JobStatus run() throws IOException, InterruptedException, ClassNotFoundException { //此句断点 step into return submitter.submitJobInternal(Job.this, cluster); }
-
断点到此条,不要step into,我们可以通过idea看到一个路径,在后续的代码运行的过程中,mr会在此路径生成一些临时的中间文件(路径中标红处为我电脑的用户名)继续运行到下一条断点:
//此句断点 Path submitJobDir = new Path(jobStagingArea, jobId.toString());
-
此条maps为mapTask的任务数量,即位切片数量,若step into,可以看到一些计算切片数量的代码,此处我们略过,继续运行去下一处断点:
//此句断点 int maps = writeSplits(job, submitJobDir);
-
此句根据其原本的备注,可知此句作用,其中submitJobFile就创建在第五点提到的路径中,不step into,去下一处核心代码:
// Write job file to submit dir //此句断点 writeConf(conf, submitJobFile);
-
又一个submit,没得说,step into,进去:
//此句断点 step into status = submitClient.submitJob( jobId, submitJobDir.toString(), job.getCredentials());
-
此处可以看到创建了一个job对象,step into进入其构造方法:
//此句断点 step into Job job = new Job(JobID.downgrade(jobid), jobSubmitDir);
-
进入该构造方法后不要急着继续运行,看到该方法最后一句:
this.start();
看到start方法,我们马上要想到找run方法,idea中ctrl+F,搜索run()方法。
如果不确定自己找到的run()是不是它将会运行的run(),可以在其第一行打断点然后继续运行,看程序是否会运行到该断点处:
-
进入正确的run方法后,向下找到该句,打断点、运行至该句,然后step into。很明显,该方法将会运行mapTask,:
//此句断点 step into runTasks(mapRunnables, mapService, "map");
此句下面不远处有一句类似的runTask,我们可以将其同样打上断点,方便之后退出。
-
进入runTask方法后,当先一个循环,我们可以稍微运行一遍该循环,然后通过idea的显示,我们可以发现其中的Runnable r是多态,直向的实际上是LocalJobRunner,这是因为我们的mr是本地运行的。该代码字面意思为向服务器提交可运行任务,此处的service为线程池,在前面的代码中创建,我们可以粗暴的将线程池对应一个集群,其中每个线程对应一台节点:
for (Runnable r : runnables) { service.submit(r); }
-
因为r是LocalJobRunner,所以我们在LocalJobRunner中搜索其run方法(如果前面的操作全部没有失误的话,此时我们应该是正好身处LocalJobRunner类中的)
-
进入该run方法后,我们可以很容易的发现一行创建MapTask对象的代码:
MapTask map = new MapTask(systemJobFile.toString(), mapId, taskId, info.getSplitIndex(), 1);
-
继续向下运行,停在此句,很明显,map任务正式开始了,我们step into,进入方法内部:
//此句断点 step into map.run(localConf, Job.this);
-
此方法内部当先是一个判断语句,判断当前任务的进程,也即是我们能在运行日志里常看到的100%、33.3%、66.7%:
if (isMapTask()) { // If there are no reducers then there won't be any sort. Hence the map // phase will govern the entire attempt's progress. if (conf.getNumReduceTasks() == 0) { mapPhase = getProgress().addPhase("map", 1.0f); } else { // If there are reducers then the entire attempt's progress will be // split between the map phase (67%) and the sort phase (33%). mapPhase = getProgress().addPhase("map", 0.667f); sortPhase = getProgress().addPhase("sort", 0.333f); } }
-
代码继续运行,来到该行,我们通过方法名判断该方法是在初始化map任务的一些配置,step into进入该方法:
//此句断点 step into initialize(job, getJobID(), reporter, useNewApi);
-
此处我们可以看到代码将任务的状态设置为了RUNNING:
setState(TaskStatus.State.RUNNING);
-
继续运行,我怕们可以看到此处生成了一个outputFormat的对象,通过idea的辅助,我们可以看到其是默认的TextOutputFormat:
outputFormat = ReflectionUtils.newInstance(taskContext.getOutputFormatClass(), job);
-
继续运行,此处的outputFormat即是我们在Driver中通过FileOutputFormat设置的输出路径:
Path outputPath = FileOutputFormat.getOutputPath(conf);
-
step out离开该方法,回到MapTask的run方法中,继续向下运行。
-
继续运行来到该句,根据方法名其开始运行了一个mapper,我们step into:
//此句断点 step into runNewMapper(job, splitMetaInfo, umbilical, reporter);
-
该方法当先三句便新建了环境对象,然后通过反射创建了Mapper对象和InputFormat对象。此处的Mapper即为我自定义的WCMapper,InputFormat应当为默认的TextInputFormat
-
继续运行到此判断语句处:
//此句断点 if (job.getNumReduceTasks() == 0) { output = new NewDirectOutputCollector(taskContext, job, umbilical, reporter); } else { output = new NewOutputCollector(taskContext, job, umbilical, reporter); }
简单的通过代码变量和方法的命名我们可以得知,如果我们设置reduce任务数为0的话,那么map阶段的输出直接为mr任务的最终输出,直接写入磁盘。
-
不要step into,继续运行到该句,简单易懂,mapper阶段的核心业务开始了,step into进入该方法:
//此句断点 step into mapper.run(mapperContext);
-
此方法代码不多且易懂,其核心代码一目了然,如果我们在此句step into,我们就来到了自定义的map方法中,如有兴趣可以自行尝试:
while (context.nextKeyValue()) { //此句断点 map(context.getCurrentKey(), context.getCurrentValue(), context); }
此处的循环即便利context中的所有键值对,每个键值对都是map方法输入的键值对,每个键值对运行一次map方法
-
至此我们基本完成了map阶段的核心业务,我们连续点击rusume program直到我们来到第11步在第二个runTasks方法处留下的断点处,根据该处代码,很明显,这是运行reduceTask的代码,step into进入该方法:
//此句断点 step into runTasks(reduceRunnables, reduceService, "reduce");
-
同mapTask运行的阶段那样,找到对应的run方法
-
此处,我们不难发现它申请了一个shuffle的空引用,看来shuffle阶段的代码依托于reduce阶段的代码,但是要注意shuffle阶段和reduce阶段不是同一个阶段:
//此句断点 ShuffleConsumerPlugin shuffleConsumerPlugin = null;
后续还会看到shuffle对象初始化的代码,并且在其初始化的方法中创建了一个归并(merge)对象,此处略过,如有兴趣可以自行尝试step into
-
继续运行,代码来到了runNewReducer方法,没得说,step into:
//此句断点 step into runNewReducer(job, umbilical, reporter, rIter, comparator, keyClass, valueClass);
-
该方法中同map阶段对应的方法那样创建了一个环境上下文context,然后通过反射创建了reducer对象和写出器RW对象
-
来到reducer.run方法,此句将运行reducer中的核心业务代码,没得说,step into,进:
//此句断点 step into reducer.run(reducerContext);
-
reducer的run方法结构类似mapper的run方法,其中的k就是map阶段输入的k,不过其v是一个基于map阶段输出的v的对应每一个相同的k的值的集合的迭代器:
while (context.nextKey()) { //此句断点 reduce(context.getCurrentKey(), context.getValues(), context); // If a back up store is used, reset it Iterator<VALUEIN> iter = context.getValues().iterator(); if(iter instanceof ReduceContext.ValueIterator) { ((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore(); } }
此处的reduce方法即运行自定义的reduce阶段
-
不停的点step out方法,直到runNewReducer方法结束,继续运行来到done方法,step into:
//此处断点 step into done(umbilical, reporter);
-
直接看该方法最后一个注释:
//signal the tasktracker that we are done
关注点:XXXXXX we are done
-
至此,恭喜你,你已经粗略的通读了一遍mr运行的业务流程的源码。
总结
简单的通读一遍代码后,对mr运转流程有了一个更清晰的理解,想必这也是很多程序员前辈强调读源码的重要性的原因。
读源码思维混乱、理不清头绪,这都不是事儿,多读两遍,对着网上的教学视频多看两遍,简单的记一记视频里讲的重点代码是那些,之后再对着笔记读一读感觉会好一点。