MapReduce详细解读一(InputFormat)

文章目录

    • MapReduce工作流程图
      • InputFormat
        • InputSplit
        • RecordReader
      • FileInputFormat
      • TextInputFormat
      • Mapper

MapReduce工作流程图

MapReduce详细解读一(InputFormat)_第1张图片

InputFormat

源码:

public abstract class InputFormat<K, V> {
    public InputFormat() {
    }

    public abstract List<InputSplit> getSplits(JobContext var1) throws IOException, InterruptedException;

    public abstract RecordReader<K, V> createRecordReader(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
}

-向Hadoop集群提交作业时,需要指定作业的输入的格式(未指认时默认的输入格式为TextInputFormat)。
-旧的APT(hadoop-0.x)中InputFormat是被定义为接口,新的API(hadoop-1.x及hadoop-2.x)中InputFormat是做为抽象类存在的
-InputFormat主要用于验证作业的输入是否符合规范,将输入的文件分割为逻辑InputSplit,每个inputSplit被分配给一个Mapper任务,提供RecordReader的实现,该实现负责从InputSplit中收集记录交给由Mapper任务处理。
-不同的InputFormat子类提供了不同的InputSplit和RecordReader。
-InputFormat抽象类只提供了两个抽象类方法,分别用于获取InputSplit和RecordReader。
-InputSplit只是对输入文件的逻辑分割,而不是物理上将输入文件分割成块,或者说InputSplit只是指定了输入文件的某个范围输入到特定的Mapper中。

-List getSplits(), 获取由输入文件计算出输入分片(InputSplit),解决数据或文件分割成片问题。
-RecordReader createRecordReader(),创建RecordReader,从InputSplit中读取数据,解决读取分片中数据问题。
-验证作业输入的正确性
-将输入文件切割成逻辑分片(InputSplit),一个InputSplit将会被分配给一个独立的MapTask
-提供RecordReader实现,读取InputSplit中的“K-V对”供Mapper使用

InputSplit

源码:

public abstract class InputSplit {
    public InputSplit() {
    }
//获取Split的大小,支持根据size对InputSplit排序.
    public abstract long getLength() throws IOException, InterruptedException;
//获取存储该分片的数据所在的节点位置,这些位置是本地的,不需要序列化。
    public abstract String[] getLocations() throws IOException, InterruptedException;
//获取有关输入拆分存储在哪些节点,以及如何在每个位置存储信息。

    @Evolving
    public SplitLocationInfo[] getLocationInfo() throws IOException {
        return null;
    }
}

-Mappers的输入是一个一个的输入分片,称InputSplit。
-nputSplit也是一个抽象类,它在逻辑上包含了提供给处理这个InputSplit的Mapper的所有K-V对。
-InputSplit的子类FileSplit类:FileSplit有四个属性:文件路径,分片起始位置,分片长度和存储分片的hosts。用这四项数据,就可以计算出提供给每个Mapper的分片数据。在InputFormat的getSplit()方法中构造分片,分片的四个属性会通过调用FileSplit的Constructor设置。
-(如果用FileSplit对应的FileInputFormat作为输入格式,那么即使文件特别小,也是作为一个单独的InputSplit来处理,而每一个InputSplit将会由一个独立的Mapper Task来处理。在输入数据是由大量小文件组成的情形下,就会有同样大量的InputSplit,从而需要同样大量的Mapper来处理,大量的Mapper Task创建销毁开销将是巨大的,甚至对集群来说,是灾难性的!)
-InputSplit的子类CombineFileSplit:CombineFileSplit是针对小文件的分片,它将一系列小文件封装在一个InputSplit内,这样一个Mapper就可以处理多个小文件。可以有效的降低进程开销。与FileSplit类似,CombineFileSplit同样包含文件路径,分片起始位置,分片大小和分片数据所在的host列表四个属性,只不过这些属性不再是一个值,而是一个列表。需要注意的一点是,CombineFileSplit的getLength()方法,返回的是这一系列数据的数据的总长度。

RecordReader

源码

public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable {
    public RecordReader() {
    }
//初始化RecordReader。
    public abstract void initialize(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
//RecordReader中最主要的方法,由它获取分片上的下一个K-V对。
    public abstract boolean nextKeyValue() throws IOException, InterruptedException;
//获取当前的Key。
    public abstract KEYIN getCurrentKey() throws IOException, InterruptedException;
//获取当前的Value。
    public abstract VALUEIN getCurrentValue() throws IOException, InterruptedException;
//记录RecordReader当前通过其数据的进展。
    public abstract float getProgress() throws IOException, InterruptedException;
//关闭RecordReader。
    public abstract void close() throws IOException;
}

FileInputFormat

public List<InputSplit> getSplits(JobContext job) throws IOException {
 		//开启线程
        Stopwatch sw = (new Stopwatch()).start();-
        //首先计算分片的最大和最小值。这两个值将会用来计算分片的大小。
        long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job));
        long maxSize = getMaxSplitSize(job);
        //splits链表用来存储计算得到的输入分片结果
        List<InputSplit> splits = new ArrayList();
        //files链表存储由listStatus()获取的输入文件列表,listStatus比较特殊
        List<FileStatus> files = this.listStatus(job);
        Iterator i$ = files.iterator();

        while(true) {
            while(true) {
                while(i$.hasNext()) {
                    FileStatus file = (FileStatus)i$.next();
                    Path path = file.getPath();
                    long length = file.getLen(); //整个文件的长度
                    //判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割
                    if (length != 0L) {
                        BlockLocation[] blkLocations;
                        if (file instanceof LocatedFileStatus) {
                            blkLocations = ((LocatedFileStatus)file).getBlockLocations();
                        } else {
                            FileSystem fs = path.getFileSystem(job.getConfiguration());
                            //获取该文件所有的block信息列表[hostname, offset, length]
                            blkLocations = fs.getFileBlockLocations(file, 0L, length);
                        }
                        //是否分割可以自行重写FileInputFormat的isSplitable来控制
                        if (this.isSplitable(job, path)) {
                            long blockSize = file.getBlockSize();
                            //计算分片大小
                            //即 Math.max(minSize, Math.min(maxSize, blockSize));
                            //也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize
                            long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);

                            long bytesRemaining;
                            int blkIndex;
                            //循环分片。当剩余数据与分片大小比值大于1.1D时,继续分片, 小于等于时,停止分片
                            for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
                                blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                                splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                            }
                            //处理余下的数据
                            if (bytesRemaining != 0L) {
                                blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                                splits.add(this.makeSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                            }
                        } else {
                        //不可split,整块返回
                            splits.add(this.makeSplit(path, 0L, length, blkLocations[0].getHosts(), blkLocations[0].getCachedHosts()));
                        }
                    } else {
                    //对于长度为0的文件,创建空Hosts列表,返回
                        splits.add(this.makeSplit(path, 0L, length, new String[0]));
                    }
                }
                //设置输入文件数量
                job.getConfiguration().setLong("mapreduce.input.fileinputformat.numinputfiles", (long)files.size());
                sw.stop();
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Total # of splits generated by getSplits: " + splits.size() + ", TimeTaken: " + sw.elapsedMillis());
                }

                return splits;
            }
        }
    }

-FileInputFormat继承InputFormat
-实现了getSplits()方法,getSplits()方法将文件切分成InputSplit,InputSplit的个数对应map()方法的个数。

TextInputFormat

源码:

public class TextInputFormat extends FileInputFormat<LongWritable, Text> {
    public TextInputFormat() {
    }

    public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
        String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");//设置record的分隔符
        byte[] recordDelimiterBytes = null;
        if (null != delimiter) {
            recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
        }

        return new LineRecordReader(recordDelimiterBytes);
    }

    protected boolean isSplitable(JobContext context, Path file) {
        CompressionCodec codec = (new CompressionCodecFactory(context.getConfiguration())).getCodec(file);
        return null == codec ? true : codec instanceof SplittableCompressionCodec;
    }
}

-TextInputFormat继承 FileInputFormat抽象类
-实现了createRecordReader()方法,createRecordReader()将InputSplit切分成可供map()处理的对。
-换行符实际上是由”textinputformat.record.delimiter”这个配置决定的
-getSplits是在逻辑上划分,并没有物理切分,也就是只是记录每个split从文件的个位置读到哪个位置,文件还是一个整体。所以在LineRecordReader中,它的处理方式是每个split多读一行,也就是读到下一个split的第一行。然后除了每个文件的第一个split,其他split都跳过第一行,进而避免重复读取,这种方式去处理。

Mapper

源码

public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
    public Mapper() {
    }
//预处理,仅在map task启动时运行一次
    protected void setup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
    }
//对于InputSplit中的每一对都会运行一次
    protected void map(KEYIN key, VALUEIN value, Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
        context.write(key, value);
    }
//扫尾工作,比如关闭流等
    protected void cleanup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
    }
//map task的驱动器
    public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
        this.setup(context);

        try {
            while(context.nextKeyValue()) {
                this.map(context.getCurrentKey(), context.getCurrentValue(), context);
            }
        } finally {
            this.cleanup(context);
        }

    }

    public abstract class Context implements MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
        public Context() {
        }
    }
}

-run()方法首先调用setup()进行初始操作
-然后循环对每个从context.nextKeyValue()获取的“K-V对”调用map()函数进行处理
-最后调用cleanup()做最后的处理

你可能感兴趣的:(源码解析,Inputformat,hadoop,mapreduce,大数据)