MapReduce之InputFormat和OutFormat

详细情况见:http://shitouer.cn/2013/02/hadoop-source-code-analyse-mapreduce-inputformat/

http://blog.sina.com.cn/s/blog_4a064aed01011lja.html

以下是对此2网址的学习:

InputFormat中的Splits集合的获取;

InputFormat是一个接口,该接口有2个成员函数;

public abstract class InputFormat<K, V> {
 
    /**
     * 仅仅是逻辑分片,并没有物理分片,所以每一个分片类似于这样一个元组 <input-file-path, start, offset>
     */
    public abstract List<InputSplit> getSplits(JobContext context)
            throws IOException, InterruptedException;
 
    /**
     * Create a record reader for a given split.
     */
    public abstract RecordReader<K, V> createRecordReader(InputSplit split,
            TaskAttemptContext context) throws IOException,
            InterruptedException;
 
}

FileInputFormat类如下

static void addInputPath(Job job, Path path) ;
         // Add a Path to the list of inputs for the map-reduce job. 增加一条输入路径
static void addInputPaths(Job job, String commaSeparatedPaths) ;
          //Add the given comma separated paths to the list of inputs for the map-reduce job. 批量增加
protected  long computeSplitSize(long blockSize, long minSize, long maxSize); 
            
protected  int getBlockIndex(BlockLocation[] blkLocations, long offset) ;
            
protected  long getFormatMinSplitSize(); 
         // Get the lower bound on split size imposed by the format. 获取最小splitSize也就是一个split有多少个blocks的最小,系统默认为1;
static PathFilter getInputPathFilter(JobContext context) ;
        //  Get a PathFilter instance of the filter set for the input paths. 获取一个文件过滤器
static Path[] getInputPaths(JobContext context) ;
         // Get the list of input Paths for the map-reduce job. 获取输入文件路径集合
static long getMaxSplitSize(JobContext context) ;
         // Get the maximum split size. 获取最大的splitSize也就是一个split有多少个blocks的最大值;
static long getMinSplitSize(JobContext job); 
          //Get the minimum split size 获取最小splitSize也就是一个split有多少个blocks的最小值;人为设置的。默认为1;
 List<InputSplit> getSplits(JobContext job) ;
         // Generate the list of files and make them into FileSplits. 获取FileSplits
protected  boolean isSplitable(JobContext context, Path filename) ;
          //Is the given filename splitable? Usually, true, but if the file is stream compressed, it will not be. 判断能否被分
protected  List<FileStatus> listStatus(JobContext job) ;
          //List input directories. 获取FileSplits的状态,描述类信息
static void setInputPathFilter(Job job, Class<? extends PathFilter> filter) ;
         // Set a PathFilter to be applied to the input paths for the map-reduce job.设置输入文件过滤器是对map-reduce job而言的 
static void setInputPaths(Job job, Path... inputPaths) ;
          //Set the array of Paths as the list of inputs for the map-reduce job.连续 设置输入文件路径是对map-reduce job而言的
static void setInputPaths(Job job, String commaSeparatedPaths); 
         // Sets the given comma separated paths as the list of inputs for the map-reduce job.隔离 设置输入文件路径是对map-reduce job而言的
static void setMaxInputSplitSize(Job job, long size) ;
         // Set the maximum split size 设置最大split size值
static void setMinInputSplitSize(Job job, long size) ;
        //  Set the minimum input split size  设置最小split size值
FileInputFormat() ;//Constructor构造函数
RecordReader<K, V> createRecordReader(InputSplit split,TaskAttemptContext context);//构造一个RecordReader对象

 

而FileInputFormat是继承了InputFormat接口的类,故而它需要实现这两个函数;

对于第一个函数实现:

public List<InputSplit> getSplits(JobContext job) throws IOException {
    // 首先计算分片的最大和最小值。这两个值将会用来计算分片的大小。
    // 由源码可知,这两个值可以通过mapred.min.split.size和mapred.max.split.size来设置
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    long maxSize = getMaxSplitSize(job);
 
    // splits链表用来存储计算得到的输入分片结果
    List<InputSplit> splits = new ArrayList<InputSplit>();
    // files链表存储由listStatus()获取的输入文件列表,listStatus比较特殊,我们在下面详细研究
    List<FileStatus> files = listStatus(job);
    for (FileStatus file : files) {
        Path path = file.getPath();
        FileSystem fs = path.getFileSystem(job.getConfiguration());
        long length = file.getLen();
        // 获取该文件所有的block信息列表[hostname, offset, length]
        BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0,
                length);
        // 判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割
        // 是否分割可以自行重写FileInputFormat的isSplitable来控制
        if ((length != 0) && isSplitable(job, path)) {
            long blockSize = file.getBlockSize();
            // 计算分片大小
            // 即 Math.max(minSize, Math.min(maxSize, blockSize));
            // 也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize
            long splitSize = computeSplitSize(blockSize, minSize, maxSize);
 
            long bytesRemaining = length;
            // 循环分片。
            // 当剩余数据与分片大小比值大于Split_Slop时,继续分片, 小于等于时,停止分片
            while (((double) bytesRemaining) / splitSize > SPLIT_SLOP) {
                int blkIndex = getBlockIndex(blkLocations, length
                        - bytesRemaining);
                splits.add(new FileSplit(path, length - bytesRemaining,
                        splitSize, blkLocations[blkIndex].getHosts()));
                bytesRemaining -= splitSize;
            }
            // 处理余下的数据
            if (bytesRemaining != 0) {
                splits.add(new FileSplit(path, length - bytesRemaining,
                        bytesRemaining,
                        blkLocations[blkLocations.length - 1].getHosts()));
            }
        } else if (length != 0) {
            // 不可split,整块返回
            splits.add(new FileSplit(path, 0, length, blkLocations[0]
                    .getHosts()));
        } else {
            // 对于长度为0的文件,创建空Hosts列表,返回
            splits.add(new FileSplit(path, 0, length, new String[0]));
        }
    }
 
    // 设置输入文件数量
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    return splits;
}

实现FileSplits集合,需要计算出一个文件的splitSize大小,也就是一个split包括多少个blocks;

Math.max(minSize, Math.min(goalSize, blockSize));

minSize表示1个splits包括最小包括多少个blocks,默认是1;

goalSize=totalblocks/numSplits,其中numSplits和maptasks数相等(一个split和1个map对应),这个是认为建议的值,而totalblocks表示所有文件(输入为1个文件,或者多个文件)在物理上所需要的blocks数;

 blocksize表示这个文件需要多少个blocksize来存放。

结论:

1. 一个split不会包含零点几或者几点几个Block,一定是包含大于等于1个整数个Block

2. 一个split不会包含两个File的Block,不会跨越File边界

3. split和Block的关系是一对多的关系

4. maptasks的个数最终决定于splits的长度

splits的loaction是取第一个block的地址信息,而不是所有block地址信息,因为有一个可以知道其他的;

详细程序过程已经注释了,这其中使用到了FlieSplit具体类,该类的构造主要是提供信息存储效果;

FileSplit类是继承InputSplit接口;主要带参构造函数和继承的两个方法,以及成员变量。

FlieSplit类如下

FlieSplit impement InputSize
{
  private Path file;      // Split所在的文件
  private long start;     // Split的起始位置
  private long length;    // Split的长度,getLength()会返回它
  private String[] hosts; // Split所在的机器名称,getLocations()会返回它
  long getLength() throws IOException
 {
   //通过这个函数返回length
   return length;
 }
  String[] getLocations() throws IOException
  {
    //........通过这个函数返回loaction
  if(host!=null)     return hosts;
else
return new String[]; } FlieSplit() {
//构造空的对象 } FlieSplit(Path file,long start,long length,String[] hosts) { //......赋值即可 file=file; start=start; length=length; hosts=hosts; } ~FlieSplit() { } }

上述的FileInputFormat的实现了InputFormat中的第一个函数,与InputSplit接口的具体类FileSplit相对应了。

另外listStatus函数实现如下;这个方法是FileInputFormat的方法,不是继承的。

protected List<FileStatus> listStatus(JobContext job) throws IOException {
 
    // 省略部分代码...
 
    List<PathFilter> filters = new ArrayList<PathFilter>();
    filters.add(hiddenFileFilter);
    PathFilter jobFilter = getInputPathFilter(job);
    if (jobFilter != null) {
        filters.add(jobFilter);
    }
    // 创建了一个MultiPathFilter,其内部包含了两个PathFilter
    // 一个为过滤隐藏文件的Filter,一个为用户自定义Filter(如果制定了)
    PathFilter inputFilter = new MultiPathFilter(filters);
 
    for (int i = 0; i < dirs.length; ++i) {
        Path p = dirs[i];
        FileSystem fs = p.getFileSystem(job.getConfiguration());
        FileStatus[] matches = fs.globStatus(p, inputFilter);
        if (matches == null) {
            errors.add(new IOException("Input path does not exist: " + p));
        } else if (matches.length == 0) {
            errors.add(new IOException("Input Pattern " + p
                    + " matches 0 files"));
        } else {
            for (FileStatus globStat : matches) {
                if (globStat.isDir()) {
                    for (FileStatus stat : fs.listStatus(
                            globStat.getPath(), inputFilter)) {
                        result.add(stat);
                    }
                } else {
                    result.add(globStat);
                }
            }
        }
    }
 
    // 省略部分代码
}

在上述中得到了FileStatus类对象的集合;FileStatus类如下

Class FileStatus extends Object implement Comparable, Writable 
{
  prviate long length;
  prviate  boolean isdir;
  prviate  int block_replication;
  prviate  long blocksize;
  prviate  long modification_time,;
  prviate long access_time;
  prviate  FsPermission permission;
  prviate  String owner;
  prviate  String group;
  prviate  Path path;

 int compareTo(Object o) ;
         // Compare this object to another object 比较
 boolean equals(Object o); 
         // Compare if this object is equal to another object 是否相等
 long getAccessTime() ;
          //Get the access time of the file. 获取access time
 long getBlockSize() ;
         // Get the block size of the file. 获取block size文件的blocks数
 String getGroup() ;
          //Get the group associated with the file. 获取文件组
 long getLen() ;//获取文件长度,单位是B;
 long getModificationTime() ;
         // Get the modification time of the file. 获取修改时间
 String getOwner() ;
          //Get the owner of the file. 获取文件所属者
 Path getPath() ;//获取文件路径
            
 FsPermission getPermission() ;
          //Get FsPermission associated with the file. 不重要
 short getReplication() ;
         // Get the replication factor of a file. 获取版本
 int hashCode() ;
          //Returns a hash code value for the object, which is defined as the hash code of the path name. 哈希后
 boolean isDir() ;
         // Is this a directory? 判断是不是路径
 void readFields(DataInput in) ;
          //Deserialize the fields of this object from in. 
protected  void setGroup(String group); 
          //Sets group. 
protected  void setOwner(String owner) ;
          //Sets owner. 
protected  void setPermission(FsPermission permission) ;
          //Sets permission. 
 void write(DataOutput out) ;
          //Serialize the fields of this object to out. 
FileStatus() ;//Constructor 
           
FileStatus(
long length, boolean isdir, int block_replication, long blocksize, long modification_time, long access_time, FsPermission permission, String owner, String group, Path path) ;//Constructor
           
FileStatus(
long length, boolean isdir, int block_replication, long blocksize, long modification_time, Path path) ;//Constructor  }


这里用到了isDir()是不是文件路径,不是路径是文件则直接添加了,该文件状态信息;若是路径则调用fs.listStatus方法将其路径下的所有文件添加到文件状态信息中。

这里又出现了PathFilter类,这些在网址1中有介绍,或者查看hadoop的api。

下面讲述第二个函数与与InputSplit接口的具体类FileSplit如何对应:

这个函数实现只需要在此种通过RecordReader接口下的具体类来构造对象,将其放回就可以得到RecordReader的具体类对象了。故而重点是用一个类去实现接口RecordReader;

首先介绍接口RecordReader

public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable {
 
    /**
     * 由一个InputSplit初始化
     */
    public abstract void initialize(InputSplit split, TaskAttemptContext context)
            throws IOException, InterruptedException;
 
    /**
     * 顾名思义,读取分片下一个<key, value>对----最重要的部分
     */
    public abstract boolean nextKeyValue() throws IOException,
            InterruptedException;
 
    /**
     * Get the current key
     */
    public abstract KEYIN getCurrentKey() throws IOException,
            InterruptedException;
 
    /**
     * Get the current value.
     */
    public abstract VALUEIN getCurrentValue() throws IOException,
            InterruptedException;
 
    /**
     * 跟踪读取分片的进度
     */
    public abstract float getProgress() throws IOException,
            InterruptedException;
 
    /**
     * Close the record reader.
     */
    public abstract void close() throws IOException;
}

具体类是LineRecordReader类,下面主要是实现了initialize和nextKeyValue方法;其他省去了

public class LineRecordReader extends RecordReader<LongWritable, Text> {
    private CompressionCodecFactory compressionCodecs = null;
    private long start;
    private long pos;
    private long end;
    private LineReader in;
    private int maxLineLength;
    private LongWritable key = null;
    private Text value = null;
 
    // initialize函数即对LineRecordReader的一个初始化
    // 主要是计算分片的始末位置,打开输入流以供读取K-V对,处理分片经过压缩的情况等
    public void initialize(InputSplit genericSplit, TaskAttemptContext context)
            throws IOException {
        FileSplit split = (FileSplit) genericSplit;
        Configuration job = context.getConfiguration();
        this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength",
                Integer.MAX_VALUE);
        start = split.getStart();
        end = start + split.getLength();
        final Path file = split.getPath();
        compressionCodecs = new CompressionCodecFactory(job);
        final CompressionCodec codec = compressionCodecs.getCodec(file);
 
        // 打开文件,并定位到分片读取的起始位置
        FileSystem fs = file.getFileSystem(job);
        FSDataInputStream fileIn = fs.open(split.getPath());
        boolean skipFirstLine = false;
        if (codec != null) {
            // 文件是压缩文件的话,直接打开文件
            in = new LineReader(codec.createInputStream(fileIn), job);
            end = Long.MAX_VALUE;
        } else {
            //
            if (start != 0) {
                skipFirstLine = true;
                --start;
                // 定位到偏移位置,下次读取就会从便宜位置开始
                fileIn.seek(start);
            }
            in = new LineReader(fileIn, job);
        }
        if (skipFirstLine) { // skip first line and re-establish "start".
            start += in.readLine(new Text(), 0,
                    (int) Math.min((long) Integer.MAX_VALUE, end - start));
        }
        this.pos = start;
    }
 
    public boolean nextKeyValue() throws IOException {
        if (key == null) {
            key = new LongWritable();
        }
        key.set(pos);// key即为偏移量
        if (value == null) {
            value = new Text();
        }
        int newSize = 0;
        while (pos < end) {
            newSize = in.readLine(value, maxLineLength,
                    Math.max((int) Math.min(Integer.MAX_VALUE, end - pos),
                            maxLineLength));
            // 读取的数据长度为0,则说明已读完
            if (newSize == 0) {
                break;
            }
            pos += newSize;
            // 读取的数据长度小于最大行长度,也说明已读取完毕
            if (newSize < maxLineLength) {
                break;
            }
            // 执行到此处,说明该行数据没读完,继续读入
        }
        if (newSize == 0) {
            key = null;
            value = null;
            return false;
        } else {
            return true;
        }
    }
    // 省略了部分方法
}

程序通过job.setInputFormatClass(FileInputFormat.class);设置自己定义的FileInputFormat输入格式

在FileInputFormat类中有了具体的LineRecordReader类对象RecordReader是如何被Mapreduce框架利用的呢?

 首先看一下基类Mapper类

public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
 
    public class Context extends MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
        public Context(Configuration conf, TaskAttemptID taskid,
                RecordReader<KEYIN, VALUEIN> reader,
                RecordWriter<KEYOUT, VALUEOUT> writer,
                OutputCommitter committer, StatusReporter reporter,
                InputSplit split) throws IOException, InterruptedException {
            super(conf, taskid, reader, writer, committer, reporter, split);//调用父类构造函数,该类就是用来构造父类,没有新的方法
        }
    }
 
    /**
     * 预处理,仅在map task启动时运行一次
     */
    protected void setup(Context context) throws IOException,
            InterruptedException {
    }
 
    /**
     * 对于InputSplit中的每一对<key, value>都会运行一次
     */
    @SuppressWarnings("unchecked")
    protected void map(KEYIN key, VALUEIN value, Context context)
            throws IOException, InterruptedException {
        context.write((KEYOUT) key, (VALUEOUT) value);
    }
 
    /**
     * 扫尾工作,比如关闭流等
     */
    protected void cleanup(Context context) throws IOException,
            InterruptedException {
    }
 
    /**
     * map task的驱动器
     */
    public void run(Context context) throws IOException, InterruptedException {
        setup(context);
        while (context.nextKeyValue()) {
            map(context.getCurrentKey(), context.getCurrentValue(), context);
        }
        cleanup(context);
    }
}

核心是在于run函数,

  • run()方法首先调用setup()进行初始操作

  • 然后循环对每个从context.nextKeyValue()获取的“K-V对”调用map()函数进行处理

  • 最后调用cleanup()做最后的处理

 Context extends MapContext表明Context的基类是MapContext

public class MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> extends
        TaskInputOutputContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
    private RecordReader<KEYIN, VALUEIN> reader;
    private InputSplit split;
 
    public MapContext(Configuration conf, TaskAttemptID taskid,
            RecordReader<KEYIN, VALUEIN> reader,
            RecordWriter<KEYOUT, VALUEOUT> writer, OutputCommitter committer,
            StatusReporter reporter, InputSplit split) {
        super(conf, taskid, writer, committer, reporter);//作用是调用父类的构造函数也就是TaskInputOutputContext构造函数完成父类构造
        this.reader = reader;
        this.split = split;
    }
 
    /**
     * Get the input split for this map.
     */
    public InputSplit getInputSplit() {
        return split;
    }
 
    @Override
    public KEYIN getCurrentKey() throws IOException, InterruptedException {
        return reader.getCurrentKey();
    }
 
    @Override
    public VALUEIN getCurrentValue() throws IOException, InterruptedException {
        return reader.getCurrentValue();
    }
 
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        return reader.nextKeyValue();
    }
 
}

我们知道一般去重写Mapper类时一般是重写它的map函数,几乎不会对run函数重写;至于setup和cleanup这些只是调用一次对于一次任务而言;高手会重写run函数;一般人只是重写map函数。

做好自己的类后,通过job来set,将自己的类给job调用使用。

job.setMapperClass(TokenizerMapper.class);//设置自己的Mapper类

job.setInputFormatClass(FileInputFormat.class);//设置自己的InputFormat类
那么关键的Mapper类中的run在哪里调用呢?又是如何传递关键参数context呢?

 待续。。。。。。。。。。。

 

 

 

 

 

 

posted @ 2014-05-20 21:43  miner007  阅读(394)  评论(0编辑  收藏  举报