前言

通读源码这东西,是每一个优秀的程序员都应该做到的基本功,学了大数据后更着网上的视频读了几次源码了,但还是毫无头绪,每次都思维混乱、痛苦不堪,全程感觉在哲学三问。
近日有老师领着又读了一遍,感觉稍微好点了,遂自己又简单的读了一遍,总算是有点头绪了,因此写下此篇,供自己和其他学习大数据的新手们在读源码时做个参考。

环境说明

  • 我所用的版本时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源码

  1. 在WCDriver中为最后的job任务提交处断点,step into
//此句断点,MapReduce程序正式开始
boolean result = job.waitForCompletion(true);
  1. 进入该方法后,我们不难发现该方法是对submit方法的封装,我们同样可以直接通过调用submit方法提交job任务,不过该封装方法提供了一些条件判断和错误处理,在submit处断点,step into
if (state == JobState.DEFINE) {
      //此句断点 step into
      submit();
    }
  1. 进入submit方法后,我们不难通过名字推测当先几行代码的功能分别是确认状态、设置使用新的API、连接、创建提交器对象:(对此类不是我们所关心的mapreduce核心流程代码,此后都略过不表,自己读源码时可以简单浏览,如果不懂可以跳过)
ensureState(JobState.DEFINE);
    setUseNewAPI();
    connect();
    final JobSubmitter submitter = 
        getJobSubmitter(cluster.getFileSystem(), 												cluster.getClient());
  1. 我们关注的应该是这一行,见名知意,可以推测其是内部提交job的方法,打断点,step into:
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
      public JobStatus run() throws IOException, InterruptedException, 
      ClassNotFoundException {
        //此句断点 step into
        return submitter.submitJobInternal(Job.this, cluster);
      }
  1. 断点到此条,不要step into,我们可以通过idea看到一个路径,在后续的代码运行的过程中,mr会在此路径生成一些临时的中间文件(路径中标红处为我电脑的用户名)继续运行到下一条断点:
//此句断点
Path submitJobDir = new Path(jobStagingArea, jobId.toString());

idea单步调试java某些断点时非常慢_ide

  1. 此条maps为mapTask的任务数量,即位切片数量,若step into,可以看到一些计算切片数量的代码,此处我们略过,继续运行去下一处断点:
//此句断点
int maps = writeSplits(job, submitJobDir);
  1. 此句根据其原本的备注,可知此句作用,其中submitJobFile就创建在第五点提到的路径中,不step into,去下一处核心代码:
// Write job file to submit dir
//此句断点
writeConf(conf, submitJobFile);
  1. 又一个submit,没得说,step into,进去:
//此句断点  step into
status = submitClient.submitJob(
          jobId, submitJobDir.toString(), job.getCredentials());
  1. 此处可以看到创建了一个job对象,step into进入其构造方法:
//此句断点 step into
Job job = new Job(JobID.downgrade(jobid), jobSubmitDir);
  1. 进入该构造方法后不要急着继续运行,看到该方法最后一句:
this.start();

看到start方法,我们马上要想到找run方法,idea中ctrl+F,搜索run()方法。

如果不确定自己找到的run()是不是它将会运行的run(),可以在其第一行打断点然后继续运行,看程序是否会运行到该断点处:

  1. 进入正确的run方法后,向下找到该句,打断点、运行至该句,然后step into。很明显,该方法将会运行mapTask,:
//此句断点 step into
runTasks(mapRunnables, mapService, "map");

此句下面不远处有一句类似的runTask,我们可以将其同样打上断点,方便之后退出。

  1. 进入runTask方法后,当先一个循环,我们可以稍微运行一遍该循环,然后通过idea的显示,我们可以发现其中的Runnable r是多态,直向的实际上是LocalJobRunner,这是因为我们的mr是本地运行的。该代码字面意思为向服务器提交可运行任务,此处的service为线程池,在前面的代码中创建,我们可以粗暴的将线程池对应一个集群,其中每个线程对应一台节点:
for (Runnable r : runnables) {
        service.submit(r);
      }
  1. 因为r是LocalJobRunner,所以我们在LocalJobRunner中搜索其run方法(如果前面的操作全部没有失误的话,此时我们应该是正好身处LocalJobRunner类中的)
  2. 进入该run方法后,我们可以很容易的发现一行创建MapTask对象的代码:
MapTask map = new MapTask(systemJobFile.toString(), mapId, taskId, info.getSplitIndex(), 1);
  1. 继续向下运行,停在此句,很明显,map任务正式开始了,我们step into,进入方法内部:
//此句断点 step into
map.run(localConf, Job.this);
  1. 此方法内部当先是一个判断语句,判断当前任务的进程,也即是我们能在运行日志里常看到的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);
      }
    }
  1. 代码继续运行,来到该行,我们通过方法名判断该方法是在初始化map任务的一些配置,step into进入该方法:
//此句断点 step into
initialize(job, getJobID(), reporter, useNewApi);
  1. 此处我们可以看到代码将任务的状态设置为了RUNNING:
setState(TaskStatus.State.RUNNING);
  1. 继续运行,我怕们可以看到此处生成了一个outputFormat的对象,通过idea的辅助,我们可以看到其是默认的TextOutputFormat:
outputFormat = ReflectionUtils.newInstance(taskContext.getOutputFormatClass(), job);
  1. 继续运行,此处的outputFormat即是我们在Driver中通过FileOutputFormat设置的输出路径:
Path outputPath = FileOutputFormat.getOutputPath(conf);
  1. step out离开该方法,回到MapTask的run方法中,继续向下运行。
  2. 继续运行来到该句,根据方法名其开始运行了一个mapper,我们step into:
//此句断点 step into
runNewMapper(job, splitMetaInfo, umbilical, reporter);
  1. 该方法当先三句便新建了环境对象,然后通过反射创建了Mapper对象和InputFormat对象。此处的Mapper即为我自定义的WCMapper,InputFormat应当为默认的TextInputFormat
  2. 继续运行到此判断语句处:
//此句断点    
if (job.getNumReduceTasks() == 0) {
      output = 
        new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
    } else {
      output = new NewOutputCollector(taskContext, job, umbilical, reporter);
    }

简单的通过代码变量和方法的命名我们可以得知,如果我们设置reduce任务数为0的话,那么map阶段的输出直接为mr任务的最终输出,直接写入磁盘。

  1. 不要step into,继续运行到该句,简单易懂,mapper阶段的核心业务开始了,step into进入该方法:
//此句断点 step into
mapper.run(mapperContext);
  1. 此方法代码不多且易懂,其核心代码一目了然,如果我们在此句step into,我们就来到了自定义的map方法中,如有兴趣可以自行尝试:
while (context.nextKeyValue()) {
  //此句断点 
  map(context.getCurrentKey(), context.getCurrentValue(), context);
}

此处的循环即便利context中的所有键值对,每个键值对都是map方法输入的键值对,每个键值对运行一次map方法

  1. 至此我们基本完成了map阶段的核心业务,我们连续点击rusume program直到我们来到第11步在第二个runTasks方法处留下的断点处,根据该处代码,很明显,这是运行reduceTask的代码,step into进入该方法:
//此句断点 step into
runTasks(reduceRunnables, reduceService, "reduce");
  1. 同mapTask运行的阶段那样,找到对应的run方法
  2. 此处,我们不难发现它申请了一个shuffle的空引用,看来shuffle阶段的代码依托于reduce阶段的代码,但是要注意shuffle阶段和reduce阶段不是同一个阶段:
//此句断点
ShuffleConsumerPlugin shuffleConsumerPlugin = null;

后续还会看到shuffle对象初始化的代码,并且在其初始化的方法中创建了一个归并(merge)对象,此处略过,如有兴趣可以自行尝试step into

  1. 继续运行,代码来到了runNewReducer方法,没得说,step into:
//此句断点 step into
runNewReducer(job, umbilical, reporter, rIter, comparator, keyClass, valueClass);
  1. 该方法中同map阶段对应的方法那样创建了一个环境上下文context,然后通过反射创建了reducer对象和写出器RW对象
  2. 来到reducer.run方法,此句将运行reducer中的核心业务代码,没得说,step into,进:
//此句断点 step into
reducer.run(reducerContext);
  1. 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阶段

  1. 不停的点step out方法,直到runNewReducer方法结束,继续运行来到done方法,step into:
//此处断点 step into
done(umbilical, reporter);
  1. 直接看该方法最后一个注释:
//signal the tasktracker that we are done

关注点:XXXXXX we are done

  1. 至此,恭喜你,你已经粗略的通读了一遍mr运行的业务流程的源码。

总结

简单的通读一遍代码后,对mr运转流程有了一个更清晰的理解,想必这也是很多程序员前辈强调读源码的重要性的原因。
读源码思维混乱、理不清头绪,这都不是事儿,多读两遍,对着网上的教学视频多看两遍,简单的记一记视频里讲的重点代码是那些,之后再对着笔记读一读感觉会好一点。