Hadoop MapReduce数据流程(上)

来源:互联网 发布:淘宝订单体检清洗过期 编辑:程序博客网 时间:2024/05/22 09:42

本文不涉及MapReduce的原理介绍,只是从源代码的层面讲讲我对Hadoop的MapReduce的执行过程、数据流的一点理解。


首先贴上一张来之于Yahoo Hadoop 教程的图片

Detailed Hadoop MapReduce data flow

 

由上图可以看出,在进入Map之前,InputFormat把存储在HDFS的文件进行读取和分割,形成和任务相关的InputSplits,然后RecordReader负责读取这些Splits,并把读取出来的内容作为Map函数的输入参数。下面我就从代码执行的角度来看,数据是如何一步步从HDFS的file到Map函数的。在Yahoo Hadoop 教程中已经详细讲解了这一过程。但我作为一个细节控,更想从源代码的级别去理清这一过程,这样我才觉得踏实,才觉得自己真真切切地掌握了这个知识点,因此我仔细阅读了这部分的源代码,写篇博客记录下来,以便以后自己查看。

首先,在Mapper类的run方法中,map函数被循环调用:

Java代码  收藏代码
  1. public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {  
  2.   
  3.      ...................................  
  4.     /** 
  5.    * Expert users can override this method for more complete control over the 
  6.    * execution of the Mapper. 
  7.    * @param context 
  8.    * @throws IOException 
  9.    */  
  10.   public void run(Context context) throws IOException, InterruptedException {  
  11.     setup(context);  
  12.     while (context.nextKeyValue()) {  
  13.       map(context.getCurrentKey(), context.getCurrentValue(), context);  
  14.     }  
  15.     cleanup(context);  
  16.   }   

          在run方法中,每调用一次context.nextKeyValue(),就执行一遍map方法,而此处的context实际上是实现了Context接口的MapContextImpl(这一点可以在MultithreadedMapper的run方法看出来),其nextKeyValue,getCurrentKey,getCurrentValue方法为:

Java代码  收藏代码
  1. @Override  
  2. public boolean nextKeyValue() throws IOException, InterruptedException {  
  3.   return reader.nextKeyValue();  
  4. }  
  5. @Override  
  6. public KEYIN getCurrentKey() throws IOException, InterruptedException {  
  7.   return reader.getCurrentKey();  
  8. }  
  9.   
  10. @Override  
  11. public VALUEIN getCurrentValue() throws IOException, InterruptedException {  
  12.   return reader.getCurrentValue();  
  13. }  
       上述代码中的实际上是由reader来完成nextKeyValue的工作,reader是RecordReader实例,RecordReader就是用来读取各个task的splits,产生map函数的输入参数。实现RecordReader接口的类由很多,那此处的reader到底是那个类的实例呢?我们到创建context的地方去看一看。
Java代码  收藏代码
  1. org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =  
  2.   new NewTrackingRecordReader<INKEY,INVALUE>  
  3.       (inputFormat.createRecordReader(split, taskContext), reporter);  
  4.   
  5. job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());  
  6. org.apache.hadoop.mapreduce.RecordWriter output = null;  
  7. ..............  
  8.   
  9. org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE>   
  10. mapContext =   
  11.   new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(),   
  12.       input, output,   
  13.       committer,   
  14.       reporter, split);  
    上面代码中的input是一个NewTrackingRecordReader实例,而NewTrackingRecordReader则是对inputFormat.createRecordReader(split, taskContext), reporter)返回的RecordReader对象的封装,inputFormat是InputFormat类的实例,InputFormat类定义了如何分割可读取文件,
Java代码  收藏代码
  1. public abstract class InputFormat<K, V> {  
  2.   
  3.    
  4.   public abstract   
  5.     List<InputSplit> getSplits(JobContext context  
  6.                                ) throws IOException, InterruptedException;  
  7.     
  8.   
  9.   public abstract   
  10.     RecordReader<K,V> createRecordReader(InputSplit split,  
  11.                                          TaskAttemptContext context  
  12.                                         ) throws IOException,   
  13.                                                  InterruptedException;  
  14.   
  15. }  
    读取文件主要是通过其创建的RecordReader来完成的。Hadoop自带了好几种输入格式,关于输入格式的具体描述可以参考此处Yahoo Hadoop 教程。JobContextImpl中包括了InputFormat的get和set方法,默认的实现是TextInputFormat---读取文件的行,行的偏移量为key,行的内容为value。我们可以通过重写InputFormat中的isSplitable和createRecordReader来实现自定义的InputFormat,并通过JobContextImpl中的set方法来在map中采用自己的输入格式。
Java代码  收藏代码
  1. @SuppressWarnings("unchecked")  
  2. public Class<? extends InputFormat<?,?>> getInputFormatClass()   
  3.    throws ClassNotFoundException {  
  4.   return (Class<? extends InputFormat<?,?>>)   
  5.     conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);  
  6. }  
 因为读物文件是通过RecordReader完成的,因此接下来看看TextInputFormat中的RecordReader是什么?
Java代码  收藏代码
  1. public class TextInputFormat extends FileInputFormat<LongWritable, Text> {  
  2.   
  3.   @Override  
  4.   public RecordReader<LongWritable, Text>   
  5.     createRecordReader(InputSplit split,  
  6.                        TaskAttemptContext context) {  
  7.     String delimiter = context.getConfiguration().get(  
  8.         "textinputformat.record.delimiter");  
  9.     byte[] recordDelimiterBytes = null;  
  10.     if (null != delimiter)  
  11.       recordDelimiterBytes = delimiter.getBytes();  
  12.     return new LineRecordReader(recordDelimiterBytes);  
  13.   }  
  14.   
  15.   @Override  
  16.   protected boolean isSplitable(JobContext context, Path file) {  
  17.          ......................  
  18.   }  
  19.   
  20. }  
    可见,TextInputFormat中,创建的RecordReader为LineRecordReader,”textinputformat.record.delimiter“指的是读取一行的数据的终止符号,即遇到“textinputformat.record.delimiter”所包含的字符时,该一行的读取结束。可以通过Configuration的set()方法来设置自定义的终止符,如果没有设置textinputformat.record.delimiter,那么Hadoop就采用以CR,LF或者CRLF作为终止符,这一点可以查看LineReader的readDefaultLine方法。查看LineRecordReader的实现就知道为什么上面说TextInputFormat是以行的偏移量为key,行的内容为value了。来看看其中的几个主要的方法:
Java代码  收藏代码
  1.   public void initialize(InputSplit genericSplit,  
  2.                        TaskAttemptContext context) throws IOException {  
  3.   FileSplit split = (FileSplit) genericSplit;  
  4.                ......  
  5.   start = split.getStart();  
  6.   end = start + split.getLength();  
  7.   final Path file = split.getPath();  
  8.   
  9.   
  10.   // open the file and seek to the start of the split  
  11.   final FileSystem fs = file.getFileSystem(job);  
  12.   fileIn = fs.open(file);  
  13.   if (isCompressedInput()) {  
  14.     decompressor = CodecPool.getDecompressor(codec);  
  15.     if (codec instanceof SplittableCompressionCodec) {  
  16.       final SplitCompressionInputStream cIn =  
  17.         ((SplittableCompressionCodec)codec).createInputStream(  
  18.           fileIn, decompressor, start, end,  
  19.           SplittableCompressionCodec.READ_MODE.BYBLOCK);  
  20.       if (null == this.recordDelimiterBytes){  
  21.         in = new LineReader(cIn, job);  
  22.       } else {  
  23.         in = new LineReader(cIn, job, this.recordDelimiterBytes);  
  24.       }  
  25.   
  26.       start = cIn.getAdjustedStart();  
  27.       end = cIn.getAdjustedEnd();  
  28.       filePosition = cIn;  
  29.     } else {  
  30.       if (null == this.recordDelimiterBytes) {  
  31.         in = new LineReader(codec.createInputStream(fileIn, decompressor),  
  32.             job);  
  33.       } else {  
  34.         in = new LineReader(codec.createInputStream(fileIn,  
  35.             decompressor), job, this.recordDelimiterBytes);  
  36.       }  
  37.       filePosition = fileIn;  
  38.     }  
  39.   } else {  
  40.     fileIn.seek(start);  
  41.     if (null == this.recordDelimiterBytes){  
  42.       in = new LineReader(fileIn, job);  
  43.     } else {  
  44.       in = new LineReader(fileIn, job, this.recordDelimiterBytes);  
  45.     }  
  46.   
  47.     filePosition = fileIn;  
  48.   }  
  49.   
  50. }  
  51. public boolean nextKeyValue() throws IOException {  
  52.   if (key == null) {  
  53.     key = new LongWritable();  
  54.   }  
  55.   key.set(pos);  
  56.   if (value == null) {  
  57.     value = new Text();  
  58.   }  
  59.   int newSize = 0;  
  60.   // We always read one extra line, which lies outside the upper  
  61.   // split limit i.e. (end - 1)  
  62.   while (getFilePosition() <= end) {  
  63.     newSize = in.readLine(value, maxLineLength,  
  64.         Math.max(maxBytesToConsume(pos), maxLineLength));  
  65.     if (newSize == 0) {  
  66.       break;  
  67.     }  
  68.     pos += newSize;  
  69.     inputByteCounter.increment(newSize);  
  70.     if (newSize < maxLineLength) {  
  71.       break;  
  72.     }  
  73.   
  74.   }  
  75.   if (newSize == 0) {  
  76.     key = null;  
  77.     value = null;  
  78.     return false;  
  79.   } else {  
  80.     return true;  
  81.   }  
  82. }  
  83.   
  84. @Override  
  85. public LongWritable getCurrentKey() {  
  86.   return key;  
  87. }  
  88.   
  89. @Override  
  90. public Text getCurrentValue() {  
  91.   return value;  
  92. }  
  首先在initialize方法里,根据传入的FileSplit来获取到当前读取文件的path,起始位置,并以此创建真正的文件读取流in,我们可以看见在nextKeyValue方法里,就是由in来读取文件,更新key和value的值。
  至此,Hadoop如何把文件数据读取出来,并以何种方式传给Map函数,就一目了然了,同时也更加理解了Yahoo Hadoop 教程里面提到的譬如FileInputFormat的默认实现,TextInputFormat是如何实现Key-Value组合等等内容。最大的好处在于,如果我要实现一些自定义的东西,我应该如何去修改代码,如何去在合适的地方嵌入自定义的东西。