MapReuce中对于文本文件的数据分片以及读取分片的源码分析

一、InputFormat抽象类

InputFormat主要用于描述输入数据的格式(这里我们只分析新API,即org.apache.hadoop.mapreduce.lib.input.InputFormat),提供以下两个功能:

  • 数据切分:按照某个策略将输入数据切分成若干个split,以便确定MapTask个数以及对应的split;
  • Mapper提供输入数据:读取给定的split的数据,解析成一个个的key/value对,供Mapper使用。

这里我们主要分析最常用的文本文件的读取方式,即TextInputFormat.java,各个类的继承关系如下所示:
在这里插入图片描述
InputFormat抽象类有两个比较重要的方法:

  • 用户获取数据分片的getSplists方法:
	List<InputSplit> getSplits(JobContext job)
  • 读取给定的split数据,生成相应的key/value对的createRecordReader方法:
	RecordReader<LongWritable, Text> createRecordReader(InputSplit split,TaskAttemptContext context)

二、如何将数据切分成split

2.1 FileInputFormat.java的getSplits()方法
  public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();
    //设定的split最小值(mapreduce.input.fileinputformat.split.minsize)
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    //设定的split最大值(mapreduce.input.fileinputformat.split.maxsize)
    long maxSize = getMaxSplitSize(job);

    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
    //获取指定路径下文件信息
    List<FileStatus> files = listStatus(job);
    for (FileStatus file: files) {
      //获取文件路径
      Path path = file.getPath();
      //文件大小(注意文件大小会超过block大小时会被分成多个block)
      long length = file.getLen();
      if (length != 0) {
        //block数组信息
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          //文件在本地
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          //文件在hdfs
          FileSystem fs = path.getFileSystem(job.getConfiguration());
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        //判断文件是否支持文件切割(一般情况下为true,但是部分压缩文件不支持切割)
        if (isSplitable(job, path)) {
          //支持文件切割
          long blockSize = file.getBlockSize();//获取文件的块大小(默认128*1024*1024)
          long splitSize = computeSplitSize(blockSize, minSize, maxSize);//获取split大小
          //切分split是对整个文件进行切分
          //将剩余未分片大小设置成整个文件大小
          long bytesRemaining = length;
          //只有剩余文件文件大小/分片大小>1.1的时候才进行文件切分
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
            //获取文件的block索引
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            //进行文件切分(进行逻辑上的切分)
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                        blkLocations[blkIndex].getHosts(),
                        blkLocations[blkIndex].getCachedHosts()));
            bytesRemaining -= splitSize;
          }

          //将剩余的数据加入分片中
          if (bytesRemaining != 0) {
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
                       blkLocations[blkIndex].getHosts(),
                       blkLocations[blkIndex].getCachedHosts()));
          }
        } else { // not splitable
          //不支持文件切割,当前文件即为一个split
          splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
                      blkLocations[0].getCachedHosts()));
        }
      } else { 
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    // Save the number of input files for metrics/loadgen
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    return splits;
  }

如上代码中使用到了File、Block以及Split,他们之间的关系如下图所示(图中未画出block的副本): 在这里插入图片描述
如上的getSplits()的代码可以使用如下流程图来表示:
在这里插入图片描述

2.2 File、Block、Splite
  • block和split的关系
    在进行文件切分的时候,我们首先会读取block块的大小以及设置的split大小,接着通过一系列的判断来确定最终的split分片大小(minSplitSize默认为1b,blockSize默认为128*1024*1024b):

    minSize = max{minSplitSize,mapred.min.split.size}
    maxSize = mapred.max.split.size
    splitSize = max{minSize,min{maxSize,blockSize}}
    
  • BlockLocation
    一般file超过指定的block的size就会将文件切分成多个块,所以在获得文件块的地址时,会返回一个BlockLocation[]数组,BlockLocation包含了block的大小,在file中的偏移量以及位置信息等,具体如下属性所示。属性中数组表示block存在多个副本,每个副本的信息都会存在BlockLocation中(详情可以看一下后面的testBlockInfo的demo)。

      private String[] hosts; // Datanode hostnames
      private String[] cachedHosts; // Datanode hostnames with a cached replica
      private String[] names; // Datanode IP:xferPort for accessing the block
      private String[] topologyPaths; // Full path name in network topology
      private long offset;  // Offset of the block in the file
      private long length;
      private boolean corrupt;
    
  • InputSplit
    getSplits()归根节点是要文件切分成List<InputSplit> splits的列表,然后交给MapTask们去处理,一个MapTask需要处理内容即为每一个InputSplit,而InputSplit并不是物理的文件切片,只是对需要处理的文件根据InputSplit大小进行逻辑上的切分,InputSplit的实现类为FileSplitFileSplit包含的信息如下所示:

    private Path file;//文件路径
    private long start;//在文件中的偏移量
    private long length;//分片大小
    private String[] hosts;//block所在的dataNode的hosts
    private SplitLocationInfo[] hostInfos;//对应hosts,并标记是否在内存中缓存
    
  • file和block的对应关系
    当file的size的大小大于block的大小时,file会被切分成多个block,具体信息如下代码示例:

        @Test
        public void testBlockInfo() throws IOException {
            Configuration conf = new Configuration();
            Path myPath = new Path("/test/hadoop-2.7.2.tar.gz");
            FileSystem hdfs=myPath.getFileSystem(conf);
            FileStatus fileStatus=hdfs.getFileStatus(myPath);
            System.out.println("file info:");
            System.out.println("---file size:"+fileStatus.getLen());
            System.out.println("---file replication:"+fileStatus.getReplication());
            System.out.println("---block of file size:"+fileStatus.getBlockSize());
    
            BlockLocation[] blockLocations=hdfs.getFileBlockLocations(fileStatus,0,fileStatus.getLen());
            System.out.println("the block info of file:");
            //指定文件被切分的block
            System.out.println("---total block num:" + blockLocations.length);
            for (int i=0;i<blockLocations.length;i++){
                System.out.println("---block index:"+i);
                //block所在dataNode名称
                System.out.println("------block names :"+ Lists.newArrayList(blockLocations[i].getNames()));
                //block所在dataNode的hosts
                System.out.println("------block hosts :"+Lists.newArrayList(blockLocations[i].getHosts()));
                System.out.println("------block cachedHosts :"+Lists.newArrayList(blockLocations[i].getCachedHosts()));
                //block的网络拓扑
                System.out.println("------block topologyPaths :"+Lists.newArrayList(blockLocations[i].getTopologyPaths()));
                //block文件大小
                System.out.println("------block length :"+Lists.newArrayList(blockLocations[i].getLength()));
                //block的偏移量
                System.out.println("------block offset :"+Lists.newArrayList(blockLocations[i].getOffset()));
            }
        }
    

    打印的信息如下所示: 在这里插入图片描述

三、如何读取Split为Mapper提供输入文件

3.1 TextInputFormat.java的createRecordReader方法

该方法返回一个RecordReader<LongWritable,Text>对象,实现类似于迭代器的功能,将文件中的每一行数据转换成key(行起始位置在file中的偏移量)/value(一行数据)的格式。其主要方法如下所示:
在这里插入图片描述
TextInputFormat.javacreateRecordReader()方法读取一个split以及task上下文信息,并返回的是一个LineRecordReader对象(RecordReader的子类)来读取split每一行的数据,具体代码如下所示:

  @Override
  public RecordReader<LongWritable, Text> 
                  createRecordReader(InputSplit split, TaskAttemptContext context) {
    String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");
    byte[] recordDelimiterBytes = null;
    if (null != delimiter)
      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
    return new LineRecordReader(recordDelimiterBytes);
  }
  • initialize方法–LineRecordReader.java
    LineRecordReader类中有一个initialize方法,该方法用于初始化,主要用于获得split相应的输入流,并且设置split分片在file中的起始位置(start/end)。
/**
     * 主要是打开输入文件获得相应的流(构建一个LineReader对象来负责读取文件内容)并且设置start/end标识
     * @param genericSplit
     * @param context the information about the task
     * @throws IOException
     */
    public void initialize(InputSplit genericSplit,
                           TaskAttemptContext context) throws IOException {
        FileSplit split = (FileSplit) genericSplit;
        Configuration job = context.getConfiguration();
        this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
        start = split.getStart();//split的开始位置
        end = start + split.getLength();//split的结束位置
        final Path file = split.getPath();//split所属文件位置
        // open the file and seek to the start of the split
        final FileSystem fs = file.getFileSystem(job);
        //获得文件输入流
        fileIn = fs.open(file);

        //获得文件压缩方式
        CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
        if (null != codec) {
            //压缩文件的处理方式
            isCompressedInput = true;
            decompressor = CodecPool.getDecompressor(codec);
            //判断压缩文件是否支持分片
            if (codec instanceof SplittableCompressionCodec) {
                final SplitCompressionInputStream cIn =
                        ((SplittableCompressionCodec) codec).createInputStream(
                                fileIn, decompressor, start, end,
                                SplittableCompressionCodec.READ_MODE.BYBLOCK);
                //获得可分片压缩文件的LineReader
                in = new CompressedSplitLineReader(cIn, job,
                        this.recordDelimiterBytes);
                start = cIn.getAdjustedStart();
                end = cIn.getAdjustedEnd();
                filePosition = cIn;
            } else {
                //获得不可分片的压缩文件的LineReader
                in = new SplitLineReader(codec.createInputStream(fileIn,
                        decompressor), job, this.recordDelimiterBytes);
                filePosition = fileIn;
            }
        } else {
            //获得非压缩文件的LineReader
            fileIn.seek(start);
            in = new UncompressedSplitLineReader(
                    fileIn, job, this.recordDelimiterBytes, split.getLength());
            filePosition = fileIn;
        }
        /**注意:
         * 如果读取的不是第一个split,那么会丢弃第一行数据,因为对于每一个split的最后一行数据会读取一个完整的行(可能会跨split读取)
         * 所以如果不是第一个split,会把每个split中的第一条不完整行记录丢弃(因为已经读过了)
         * */
        if (start != 0) {
            start += in.readLine(new Text(), 0, maxBytesToConsume(start));
        }
        this.pos = start;
    }

上面代码需要注意的是start的起始位移的设置,一开始start=split.getStart();,后续如果判断当前split不是初试split则会重置start的位置,为什么会这样,会在后续3.2中介绍。

  • nextKeyValue方法–LineRecordReader.java
    该方法主要用于一行行读取文件具体的内容,key是split相对于file的偏移量,而value则是具体的行数据。
/**
     * 获得key以及value的值,key为当前行在文件中的偏移量,value为行数据
     *
     * @return
     * @throws IOException
     */
    public boolean nextKeyValue() throws IOException {
        if (key == null) {
            key = new LongWritable();
        }
        key.set(pos);
        if (value == null) {
            value = new Text();
        }
        int newSize = 0;

        // We always read one extra line, which lies outside the upper
        // split limit i.e. (end - 1)
        while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
            //while循环中的代码保证了跨split的行读取的完整性
            if (pos == 0) {
                newSize = skipUtfByteOrderMark();
            } else {
      //读取文件中的行数据,并复制给value
                newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
                pos += newSize;
            }

            if ((newSize == 0) || (newSize < maxLineLength)) {
                break;
            }

            // line too long. try again
            LOG.info("Skipped line of size " + newSize + " at pos " +
                    (pos - newSize));
        }
        if (newSize == 0) {
            key = null;
            value = null;
            return false;
        } else {
            return true;
        }
    }

这里面有一个需要注意的地方就是,存在file中的一行数据跨split的情况,这就要求在读取split中最后一行数据的时候,会读取下一个split中其起始行的数据,具体情况会在后续的3.2中介绍。

在之前的initialize方法中,我们根据文件类型的不同生成了三种不同的LineReader,而在nextKeyValue方法中我们会根据不同的文件类型的输入流的调用不同的readLine方法,不同类型的LineReader的类图如下所示,即:

  • SplitLineReader:处理可以切割的数据
  • CompressedSplitLineReader:处理压缩数据
  • UnCompressedSplitLineReader:处理不可压缩的数据
    在这里插入图片描述
    而归根结底最终都会去调用LineReader类中的readLine方法,下面我们就来看一下readLine的方法实现逻辑:
  public int readLine(Text str, int maxLineLength,
                      int maxBytesToConsume) throws IOException {
    if (this.recordDelimiterBytes != null) {
    	//使用自定义的分隔符读取行数据
      return readCustomLine(str, maxLineLength, maxBytesToConsume);
    } else {
    	//使用默认的分隔符(\r\n)读取行数据
      return readDefaultLine(str, maxLineLength, maxBytesToConsume);
    }
  }
3.2 行跨split的读取方式

LineRecordReader.java类中initialize方法展示了如何进行行跨split的读取方式,当读取split的最后一行数据的时候,我们会将下一个split的第一个不完整行给一起读取,接着在读取后面的每一个split的时候,第一个不完整行会被丢弃,直接从第二行开始读取。如下入所示:
在这里插入图片描述
在读取block第三行数据的时候,split1只有该行数据的前半部分,所以为了数据的完整性,会进行跨split进去读取数据,即读取完整的hello world3。那么在读取split2的时候,由于world3已经读取过了,所以丢弃split2中的第一行数据。读取split2时,需要从hello world4开始读取。

3.3 split的跨block的读取方式

实际中当split的大小分配不恰当的时候,会造成一个splitblock(可以参考之前介绍的block和split的关系)。我们前面已经讲过split是逻辑分片,不是物理分片,当MapTask的数据本地性发挥作用时,会从本机的block开始读取,超过这个block的部分可能还在本机也可能不在本机,如果是后者的话就要从别的节点拉数据过来,因为实际获取数据是一个输入流,这个输入流面向的是整个文件,不受block和split的影响,split的大小越大可能需要从别的节点拉的数据越多,从从而效率也会越慢,拉数据的多少是由getSplits方法中的splitSize决定的(如下图所示,在处理完split1中的block1之后,还需要在其它的datanode2上去获取block2中的部分数据)。所以为了更有效率,在设置splitSize的时候应该尽量将splitSize的大小设置成blockSize的大小。
在这里插入图片描述

参考资料

MapReduce中TextInputFormat分片和读取分片数据源码级分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值