6.MR核心_FilelnputFormat

FilelnputFormat是针对文件的基础类,它继承自InputFormat类。InputFormat是一个抽象类,所有的输入格式类都继承自InputFormat。它的子类有专门用于读取普通文件的FileInputFormat,还有用来读取数据库的DBInputFormat等等。

/**
 * Map-Reduce框架依赖于作业的InputFormat:
 *	 1.验证Job的输入规范。
 *	 2.将输入文件切分为逻辑input split,每个input split都被分配给一个单独的mapper
 *	 3.提供RecordReader实现,用于从逻辑InputSplit中收集输入记录,以便mapper进行处理。
 */
@InterfaceAudience.Public
@InterfaceStability.Stable
public abstract class InputFormat {

  /** 
   * 将Job逻辑拆分为Input File Splits。
   */
  public abstract 
    List getSplits(JobContext context) throws IOException,InterruptedException;
  
  /**
   * 为给定的split创建一个记录阅读器。
   * 框架将在使用split之前调用RecordReader#initialize(InputSplit, taskabortcontext)。
   */
  public abstract RecordReader 
	createRecordReader(InputSplit split,TaskAttemptContext context) throws IOException,InterruptedException;

}

我们看到InputFormat主要是得到InputSplit分片,将InputSplit分片分配给mapper任务。而InputSplit类也是一个抽象类,它在逻辑上包含了提供给处理这个InputSplit的Mapper的所有K-V对。

/**
 * InputSplit表示由单个Mapper处理的数据
 */
@InterfaceAudience.Public
@InterfaceStability.Stable
public abstract class InputSplit {
  /**
   * 获取Split的大小,以便Input Split可以按大小排序
   */
  public abstract long getLength() throws IOException, InterruptedException;

  /**
   * 按名称获取本地用于分割的数据的节点列表
   */
  public abstract 
    String[] getLocations() throws IOException, InterruptedException;
  
  /**
   * 获取关于Input Split存储在哪个节点上以及如何在每个位置存储该节点的信息
   */
  @Evolving
  public SplitLocationInfo[] getLocationInfo() throws IOException {
    return null;
  }
}

我们先深入看一下InputSplit的FileSplit子类:

public class FileSplit extends InputSplit implements Writable {
  private Path file;
  private long start;
  private long length;
  private String[] hosts;
  private SplitLocationInfo[] hostInfos;

  public FileSplit() {}

  /** Constructs a split with host information
   *
   * @param file the file name
   * @param start the position of the first byte in the file to process
   * @param length the number of bytes in the file to process
   * @param hosts the list of hosts containing the block, possibly null
   */
  public FileSplit(Path file, long start, long length, String[] hosts) {
    this.file = file;
    this.start = start;
    this.length = length;
    this.hosts = hosts;
  }
  /** The file containing this split's data. */
  public Path getPath() { return file; }
  
  /** The position of the first byte in the file to process. */
  public long getStart() { return start; }
  
  /** The number of bytes in the file to process. */
  @Override
  public long getLength() { return length; }
}

通过查看源码可以看出,每个FileSplit都由文件路径、分片起始位置、分片长度、分片存储的hosts。有这几个属性通过构造方法设置值,就可以计算出每个Split的分片数据了。之后在FileInputFormat中调用获取File Splits分片。

接下来我们再看一个FileSplit的子类CombineFileSplit。学习该类的原因是:该类是针对小文件的分片。因为FileSplit类会将每个小文件切分成一个Split,分配给一个Mapper任务执行。如果有大量小文件存在的话,就会产生大量的Split,需要创建大量的Mapper Task去处理。这样的开销对于集群是非常不利的!对于小文件切片我们使用CombineFileSplit,它将一系列小文件放到一个Split片,交给一个Mapper Task处理,有效的降低了进程开销。我们看到,CombineFileSplit返回的属性不是单个属性,而是一个数组。

public class CombineFileSplit extends InputSplit implements Writable {

  private Path[] paths;
  private long[] startoffset;
  private long[] lengths;
  private String[] locations;
  private long totLength;

  public CombineFileSplit() {}
  public CombineFileSplit(Path[] files, long[] start, long[] lengths, String[] locations) {
    initSplit(files, start, lengths, locations);
  }

  public CombineFileSplit(Path[] files, long[] lengths) {
    long[] startoffset = new long[files.length];
    for (int i = 0; i < startoffset.length; i++) {
      startoffset[i] = 0;
    }
    String[] locations = new String[files.length];
    for (int i = 0; i < locations.length; i++) {
      locations[i] = "";
    }
    initSplit(files, startoffset, lengths, locations);
  }
  
  private void initSplit(Path[] files, long[] start, long[] lengths, String[] locations) {
    this.startoffset = start;
    this.lengths = lengths;
    this.paths = files;
    this.totLength = 0;
    this.locations = locations;
    for(long length : lengths) {
      totLength += length;
    }
  }

  public long getLength() {
    return totLength;
  }

  /** Returns an array containing the start offsets of the files in the split*/ 
  public long[] getStartOffsets() {
    return startoffset;
  }
  
  /** Returns an array containing the lengths of the files in the split*/ 
  public long[] getLengths() {
    return lengths;
  }
  //省略了一些其他的信息
}

注意:CombineFileSplit的getLength()方法,返回的是这一系列数据的数据的总长度。

通过以上几个InputSplit的子类我们知道了分片的属性。分片是在InputFormat中实现的,接下来重点看一下FileInputFormat类

public List 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 splits = new ArrayList();
    // files链表存储由listStatus()获取的输入文件列表,listStatus比较特殊,我们在下面详细研究
    List 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;
}

通过getSplits(...)方法,我们看出影响分片数量的因素有:文件是否可切分、文件大小、文件数量、Block Size大小。(当然,除了默认切分方式之外,还有其他切分方式,比如:按行切分。NLineInputFormat我们下文介绍。)在该方法中,我们看到一个listStatus(...)方法。

  /** 列出所有输入目录.
   */
  protected List listStatus(JobContext job) throws IOException {
    Path[] dirs = getInputPaths(job);
    if (dirs.length == 0) {
      throw new IOException("No input paths specified in job");
    }
    
    // 获取所有所需文件系统的令牌
    TokenCache.obtainTokensForNamenodes(job.getCredentials(), dirs, job.getConfiguration());

    // 是否需要递归
    boolean recursive = getInputDirRecursive(job);

    // 使用hiddenFileFilter和用户自定义过滤器创建一个MultiPathFilter
    List filters = new ArrayList();
    filters.add(hiddenFileFilter);
    PathFilter jobFilter = getInputPathFilter(job);
    if (jobFilter != null) {
      filters.add(jobFilter);
    }
    PathFilter inputFilter = new MultiPathFilter(filters);
    
    List result = null;

    int numThreads = job.getConfiguration().getInt(LIST_STATUS_NUM_THREADS,
        DEFAULT_LIST_STATUS_NUM_THREADS);
    StopWatch sw = new StopWatch().start();
    if (numThreads == 1) {
      result = singleThreadedListStatus(job, dirs, inputFilter, recursive);
    } else {
      Iterable locatedFiles = null;
      try {
        LocatedFileStatusFetcher locatedFileStatusFetcher = new LocatedFileStatusFetcher(
            job.getConfiguration(), dirs, recursive, inputFilter, true);
        locatedFiles = locatedFileStatusFetcher.getFileStatuses();
      } catch (InterruptedException e) {
        throw new IOException("Interrupted while getting file statuses");
      }
      result = Lists.newArrayList(locatedFiles);
    }
    
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Time taken to get FileStatuses: "
          + sw.now(TimeUnit.MILLISECONDS));
    }
    LOG.info("Total input paths to process : " + result.size()); 
    return result;
  }

我们看到在获取所有输入目录的文件路径前,先做了文件过滤。我们看看PathFilter文件过滤器,PathFilter是一个文件过滤接口,控制哪些文件可以作为输入,哪些文件不可以。可以作为输入的话,accept(...)返回True。我们也可以通过参数mapred.input.pathFilter.class指定自定义的文件过滤类。

public interface PathFilter {
  /**
   * 测试指定的抽象路径名是否应该包含在路径名列表中
   */
  boolean accept(Path path);
}

// hiddenFileFilter属性
private static final PathFilter hiddenFileFilter = new PathFilter(){
  public boolean accept(Path p){
    String name = p.getName(); 
    return !name.startsWith("_") && !name.startsWith("."); 
  }
}; 

/**
* 相当于代理路径过滤器,通过构造方法中所有的过滤器过滤后才能确定Path能否作为输入。
*/
private static class MultiPathFilter implements PathFilter {
    private List filters;

    public MultiPathFilter(List filters) {
      this.filters = filters;
    }

    public boolean accept(Path path) {
      for (PathFilter filter : filters) {
	    if (!filter.accept(path)) {
	      return false;
	    }
      }
      return true;
    }
}

    // 在listStatus中,传入MultiPathFilter中两个过滤器,一个是隐藏过滤器和一个自定义过滤器(如果有的话)。
    List filters = new ArrayList();
    filters.add(hiddenFileFilter);
    PathFilter jobFilter = getInputPathFilter(job);
    if (jobFilter != null) {
      filters.add(jobFilter);
    }
    PathFilter inputFilter = new MultiPathFilter(filters);

由此可以看出内部静态类MultiPathFilter类似与PathFilter的一个代理,所有的过滤器执行都在它内部accept(...)方法中调用。

刚开始咱们介绍FileInputFormat的时候说过,它可以验证Job的输入规范、将输入文件切片、实现RecordReader提供Mapper读取分片。我们已经看到它使用PathFilter验证输入规范、通过getSplits(...)计算了输入分片,它还有另一个方法createRecordReader(...),该方法没有在FileInputFormat中实现,在其子类中实现的。我们以NLineInputFormat为例(接上上文留的坑):

// NLineInputFormat,它将N行输入分割为一段
public class NLineInputFormat extends FileInputFormat { 
  // 省略....
  
 // 实现了为Mapper Task提供分片的方法
  public RecordReader createRecordReader(
      InputSplit genericSplit, TaskAttemptContext context) 
      throws IOException {
    context.setStatus(genericSplit.toString());
    return new LineRecordReader();
  }

  /** 
   * 逻辑上划分Job的一系列输入文件,将N行输入切分成一个split
   */
  public List getSplits(JobContext job)
  throws IOException {
    List splits = new ArrayList();
    int numLinesPerSplit = getNumLinesPerSplit(job);
    for (FileStatus status : listStatus(job)) {
      splits.addAll(getSplitsForFile(status,
        job.getConfiguration(), numLinesPerSplit));
    }
    return splits;
  }

我们看到,NLineInputFormat类实现了createRecordReader(...)方法,返回了一个LineRecordReader对象表示N行记录。同时,在getSplits(...)方法中,通过每个切片的行数来切分。我们重点关注一下LineRecordReader,它是RecordReader的子类。

/**
 * 记录阅读器将数据分解为对,以供Mapper输入.
 * @param 
 * @param 
 */
public abstract class RecordReader implements Closeable {

  // 初始化
  public abstract void initialize(InputSplit split,TaskAttemptContext context) throws IOException, InterruptedException;

  // 读取下一个
  public abstract 
  boolean nextKeyValue() throws IOException, InterruptedException;

  // 得到当前key
  public abstract
  KEYIN getCurrentKey() throws IOException, InterruptedException;
  
  // 得到当前value
  public abstract 
  VALUEIN getCurrentValue() throws IOException, InterruptedException;
  
 // 得到当前分片进度
  public abstract float getProgress() throws IOException, InterruptedException;
  
  public abstract void close() throws IOException;
}

通过源码我们可以看到RecordReader主要就是定义了这几个方法,LineRecordReader主要就是实现这几个方法。

public class LineRecordReader extends RecordReader {
    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;
        }
    }
    // 省略了部分方法
}

到此为止,我们已经为Mapper准备好了切片。下一章我们分析一下Mapper,看看MapReduce框架是如何处理RecordReader的。

你可能感兴趣的:(MapReduce)