• 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呢?

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

  • 相关阅读:
    OpenFire源码学习之十九:在openfire中使用redis插件(上)
    OpenFire源码学习之十八:IOS离线推送
    OpenFire源码学习之十七:HTTP Service插件
    OpenFire源码学习之十六:wildfire
    OpenFire源码学习之十五:插件开发
    OpenFire源码学习之十四:插件管理
    OpenFire源码学习之十三:消息处理
    数据挖掘入门
    iOS小技巧
    图片加载完成之前对图片高度侦测
  • 原文地址:https://www.cnblogs.com/miner007/p/3740103.html
Copyright © 2020-2023  润新知