MapReduce学习笔记之输入(二)

本文详细介绍了MapReduce的输入处理,包括InputFormat类、InputSplit类、RecordReader类的功能和实现,以及如何处理Hadoop的"小文件"问题。通过Hadoop Archive、Sequence file和CombineFileInputFormat三种策略解决小文件带来的效率问题。此外,还讨论了输入过滤和Mapper的工作流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类图
InputFormat

1. InputFormat类

MapReduce作业的输入数据的规格是通过InputFormat类及其子类给出的。有以下几项主要功能:

  1. 输入数据的有效性检测。
  2. 将输入数据切分为逻辑块(InputSplit),并把他们分配给对应的Map任务。
  3. 实例化一个能在每个InputSplit类上工作的RecordReader对象,并以键-值对方式生成数据,这些K-V对将由我们写的Mapper方法处理。
派生类说明
FileInputFormat从HDFS中获取输入
DBInputFormat从支持SQL的数据库读取数据的特殊类
CombineFileInputFormat将对各文件合并到一个分片中


其中TextInputFormat是Hadoop默认的输入方法,而这个是继承自FileInputFormat的。之后,每行数据都会生成一条记录,每条记录则表示成

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子类

分片方法代码及详细注释如下:

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()获取的输入文件列表
    List<FileStatus> files = listStatus(job);
    for (FileStatus file: files) {
        Path path = file.getPath();
        long length = file.getLen();
        if (length != 0) {
            // 获取该文件所有的block信息列表[hostname, offset, length]
            BlockLocation[] blkLocations;
            if (file instanceof LocatedFileStatus) {
                blkLocations = ((LocatedFileStatus) file).getBlockLocations();
            } else {
                FileSystem fs = path.getFileSystem(job.getConfiguration());
                blkLocations = fs.getFileBlockLocations(file, 0, length);
            }
            // 判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割
            // 是否分割可以自行重写FileInputFormat的isSplitable来控制
            if (isSplitable(job, path)) {
                // 计算分片大小
                // 即 Math.max(minSize, Math.min(maxSize, blockSize));
                // 也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize
                long blockSize = file.getBlockSize();
                long splitSize = computeSplitSize(blockSize, minSize, maxSize);

                // 循环分片。
                // 当剩余数据与分片大小比值大于Split_Slop时,继续分片, 小于等于时,停止分片
                long bytesRemaining = length;
                while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
                    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 {
            // 对于长度为0的文件,创建空Hosts列表,返回
            //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());

    return splits;
}

2. InputSplit类

抽象类InputSplit及其派生类有以下几个主要属性:

  1. 输入文件名。
  2. 分片数据在文件中的偏移量。
  3. 分片数据的长度(以字节为单位)。
  4. 分片数据所在的节点的位置信息。

在HDFS中,当文件大小少于HDFS的块容量时,每个文件将创建一个InputSplit实例。而对于被分割成多个块的文件,将使用更复杂的公式来计算InputSplit的数量。

基于分片所在位置信息和资源的可用性,调度器将决定在哪个节点为一个分片执行对应的Map任务,然后分片将与执行任务的节点进行通信。

InputSplit源码

public abstract class 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;
  }
}

FileSplit子类

public class FileSplit extends InputSplit implements Writable {
  private Path file; // 文件路径
  private long start; // 分片起始位置
  private long length; // 分片长度
  private String[] hosts; // 存储分片的hosts
  private SplitLocationInfo[] hostInfos; // 存储分片的信息信息

  public FileSplit() {}

  /** 
   * Constructs a split with host information
   */
  public FileSplit(Path file, long start, long length, String[] hosts) {
    this.file = file;
    this.start = start;
    this.length = length;
    this.hosts = hosts;
  }

  /** Constructs a split with host and cached-blocks 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
  * @param inMemoryHosts the list of hosts containing the block in memory
  */
 public FileSplit(Path file, long start, long length, String[] hosts,
     String[] inMemoryHosts) {
   this(file, start, length, hosts);
   hostInfos = new SplitLocationInfo[hosts.length];
   for (int i = 0; i < hosts.length; i++) {
     // because N will be tiny, scanning is probably faster than a HashSet
     boolean inMemory = false;
     for (String inMemoryHost : inMemoryHosts) {
       if (inMemoryHost.equals(hosts[i])) {
         inMemory = true;
         break;
       }
     }
     hostInfos[i] = new SplitLocationInfo(hosts[i], inMemory);
   }
 }

  // 略掉部分方法
}

FileSplit有四个属性:文件路径,分片起始位置,分片长度和存储分片的hosts。用这四项数据,就可以计算出提供给每个Mapper的分片数据。在InputFormat的getSplit()方法中构造分片,分片的四个属性会通过调用FileSplit的Constructor设置。

3. RecordReader类

RecordReader将读入到Map的数据拆分成K-V对。

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,来了解各功能的具体操作:

public class LineRecordReader extends RecordReader<LongWritable, Text> {
    public static final String MAX_LINE_LENGTH =
            "mapreduce.input.linerecordreader.line.maxlength";

    private long start;
    private long pos;
    private long end;
    private SplitLineReader in;
    private FSDataInputStream fileIn;
    private Seekable filePosition;
    private int maxLineLength;
    private LongWritable key;
    private Text value;
    private boolean isCompressedInput;
    private Decompressor decompressor;
    private byte[] recordDelimiterBytes;

    public LineRecordReader() {
    }

    public LineRecordReader(byte[] recordDelimiter) {
        this.recordDelimiterBytes = recordDelimiter;
    }

    // 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(MAX_LINE_LENGTH, Integer.MAX_VALUE);
        start = split.getStart();
        end = start + split.getLength();
        final Path file = split.getPath();

        // 打开文件,并定位到分片读取的起始位置
        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);
                in = new CompressedSplitLineReader(cIn, job,
                        this.recordDelimiterBytes);
                start = cIn.getAdjustedStart();
                end = cIn.getAdjustedEnd();
                filePosition = cIn;
            } else {
                in = new SplitLineReader(codec.createInputStream(fileIn,
                        decompressor), job, this.recordDelimiterBytes);
                filePosition = fileIn;
            }
        } else {
            // 定位到偏移位置,下次读取就会从此位置开始
            fileIn.seek(start);
            in = new UncompressedSplitLineReader(
                    fileIn, job, this.recordDelimiterBytes, split.getLength());
            filePosition = fileIn;
        }
        // If this is not the first split, we always throw away first record
        // because we always (except the last split) read one extra line in
        // next() method.
        if (start != 0) {
            start += in.readLine(new Text(), 0, maxBytesToConsume(start));
        }
        this.pos = start;
    }


    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()) {
            if (pos == 0) {
                newSize = skipUtfByteOrderMark();
            } else {
                newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
                pos += newSize;
            }

            // 读取的数据长度为0,则说明已读完
            if ((newSize == 0) || (newSize < maxLineLength)) {
                break;
            }
        }
        if (newSize == 0) {
            key = null;
            value = null;
            return false;
        } else {
            return true;
        }
    }

    // 省略了部分方法
}

4. 输入过滤

通常,一个作业的输入都需要基于某些属性进行过滤。数据层面的过滤可以在Map里面完成,这种过滤使得Map任务仅需要对感兴趣的文件进行处理,并能减少不必要的文件读取。

PathFilter文件筛选器接口,使用它我们可以控制哪些文件要作为输入,哪些不作为输入。PathFilter有一个accept(Path)方法,当接收的Path要被包含进来,就返回true,否则返回false。

public interface PathFilter {
  /**
   * Tests whether or not the specified abstract pathname should be
   * included in a pathname list.
   *
   * @param  path  The abstract pathname to be tested
   * @return  <code>true</code> if and only if <code>pathname</code>
   *          should be included
   */
  boolean accept(Path path);
}

FileInputFormat类有hiddenFileFilter属性:

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

5. Mapper

public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

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

    /**
     * 预处理,仅在map task启动时运行一次
     */
    protected void setup(Context context
    ) throws IOException, InterruptedException {
        // NOTHING
    }

    /**
     * 对于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 {
        // NOTHING
    }

    /**
     * map task的驱动器
     */
    public void run(Context context) throws IOException, InterruptedException {
        setup(context);
        try {
            while (context.nextKeyValue()) {
                map(context.getCurrentKey(), context.getCurrentValue(), context);
            }
        } finally {
            cleanup(context);
        }
    }
}

Mapper.class中的run()方法,它相当于map task的驱动:

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

6. Hadoop的“小文件”问题

当输入文件明显小于HDFS的块容量时,就会出现“小文件”文件。小文件作为输入处理时,Hadoop将为每个文件创建一个Map任务,这将引入很高的任务注册开销。而且每个文件在NameNode中大约占据150字节的内存。

可以采取以下策略处理小文件:分别为:Hadoop Archive,Sequence file和CombineFileInputFormat。

6.1 Hadoop Archive

Hadoop Archive或者HAR,是一个高效地将小文件放入HDFS块中的文件存档工具,它能够将多个小文件打包成一个HAR文件,这样在减少namenode内存使用的同时,仍然允许对文件进行透明的访问。

使用HAR时需要两点,第一,对小文件进行存档后,原文件并不会自动被删除,需要用户自己删除;第二,创建HAR文件的过程实际上是在运行一个mapreduce作业,因而需要有一个hadoop集群运行此命令。


该方案需人工进行维护,适用管理人员的操作,而且har文件一旦创建,Archives便不可改变,不能应用于多用户的互联网操作。

使用例:

# 创建输出路径
hdfs dfs -mkdir /hw/hdfs/mr/combine/archiveDir
# 创建archive文件
hadoop archive -archiveName pack1.har -p /hw/hdfs/mr/combine awd_2003_12 output/part-m-00000 /hw/hdfs/mr/combine/archiveDir
# 查看包内容
hdfs dfs -ls -R har:///hw/hdfs/mr/combine/archiveDir/pack1.har
# 查看目录内容
hdfs dfs -ls har:///hw/hdfs/mr/combine/archiveDir/pack1.har/awd_2003_12
# 查看文件内容
hdfs dfs -cat har:///hw/hdfs/mr/combine/archiveDir/pack1.har/output/part-m-00000

6.2 Sequence file

SequeceFile是Hadoop API提供的一种二进制文件支持。这种二进制文件直接将K-V对序列化到文件中。一般对小文件可以使用这种文件合并,即将文件名作为key,文件内容作为value序列化到大文件中。这种文件格式有以下好处:

  1. 支持压缩,且可定制为基于Record或Block压缩(Block级压缩性能较优)
  2. 本地化任务支持:因为文件可以被切分,因此MapReduce任务时数据的本地化情况应该是非常好的。
  3. 难度低:因为是Hadoop框架提供的API,业务逻辑侧的修改比较简单。

坏处是需要一个合并文件的过程,且合并后的文件将不方便查看。


该方案对于小文件的存取都比较自由,不限制用户和文件的多少,但是SequenceFile文件不能追加写入,适用于一次性写入大量小文件的操作。

SequenceFile分别提供了读、写、排序的操作类。SequenceFile的操作中有三种处理方式:

  1. NONE:不压缩数据直接存储
  2. RECORD:压缩value值不压缩key值存储的存储方式
  3. BLOCK:key/value值都压缩的方式存储

示例:

public class SequenceFileDemo {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(new URI("hdfs://hsm01:9000"), conf);

        //输入路径:文件夹
        FileStatus[] files = fs.listStatus(new Path(args[0]));

        Text key = new Text();
        Text value = new Text();

        //输出路径:文件
        SequenceFile.Writer writer = SequenceFile.createWriter(fs, conf, new Path(args[1]), key.getClass(), value.getClass());
        InputStream in = null;
        byte[] buffer = null;

        for (FileStatus file : files) {
            key.set(file.getPath().getName());
            in = fs.open(file.getPath());
            buffer = new byte[(int) file.getLen()];
            IOUtils.readFully(in, buffer, 0, buffer.length);
            value.set(buffer);
            IOUtils.closeStream(in);
            System.out.println(key.toString() + "\n" + value.toString());
            writer.append(key, value);
        }

        IOUtils.closeStream(writer);
    }
}

6.3 CombineFileInputFormat

使用CombineFileInputFormat将对各小文件合并到一个InputSplit中。但由于没有改变NameNode中的文件数量,所以它不能减轻NameNode的内存需求量的压力。

下面演示CombineFileInputFormat将多个小文件合并:

CustomCombineFileInputFormat.java

/**
 * 合并文件,唯一需要重写的是createRecordReader()方法.
 * <p>
 *     在getSplits()方法中返回一个CombineFileSplit分片对象.每个分片可能合并了来自不同文件的不同块.
 *     如果使用setMaxSplitSize()方法设置了分片的最大容量,本地节点的文件将会合并到一个分片,超出分片最大
 *     容量的部分将与同一机架的其他主机的块合并.
 *     如果没有设置这个最大容量,合并只会在同一机架内进行.
 * </p>
 *
 * Created by zhangws on 16/10/8.
 */
public class CustomCombineFileInputFormat extends CombineFileInputFormat<LongWritable, Text> {

    @Override
    public RecordReader<LongWritable, Text> createRecordReader(InputSplit split,
                                                               TaskAttemptContext context)
            throws IOException {

        return new CombineFileRecordReader<>((CombineFileSplit) split,
                context, CustomCombineFileRecordReader.class);
    }
}

CustomCombineFileRecordReader.java

/**
 * 从CombineFileSplit中返回记录.
 * <p>
 *     CombineFileSplit与FileSplit之间的不同点在于是否存在包含多个偏移量和长度的多个路径.
 *
 *     自定义的RecordReader类会被分片中的每个文件调用,因此,自定义RecordReader类的构造函数
 *     必须有一个整型变量指明特定的文件正在用于产生记录.
 *
 *     第二个很重要的方法是nextKeyValue(),它负责产生下一个K-V对,getCurrentKey()与getCurrentValue()
 *     方法返回这个K-V对.
 * </p>
 *
 * Created by zhangws on 16/10/8.
 */
public class CombineFileRecordReader extends RecordReader<LongWritable, Text> {

    private LongWritable key; // 当前位置在文件中的字节偏移量
    private Text value; // 当前所在行的文本
    private Path path;
    private FileSystem fileSystem;
    private LineReader lineReader; // 读取每一行数据
    private FSDataInputStream fsDataInputStream;
    private Configuration configuration;
    private int fileIndex;
    private CombineFileSplit combineFileSplit;
    private long start;
    private long end;

    public CombineFileRecordReader(CombineFileSplit combineFileSplit,
                                   TaskAttemptContext taskAttemptContext,
                                   Integer index) throws IOException {
        this.fileIndex = index;
        this.combineFileSplit = combineFileSplit;
        this.configuration = taskAttemptContext.getConfiguration();
        this.path = combineFileSplit.getPath(index);
        this.fileSystem = this.path.getFileSystem(this.configuration);
        this.fsDataInputStream = fileSystem.open(this.path);
        this.lineReader = new LineReader(this.fsDataInputStream, this.configuration);
        this.start = combineFileSplit.getOffset(index);
        this.end = this.start + combineFileSplit.getLength(index);
        this.key = new LongWritable(0);
        this.value = new Text("");
    }

    @Override
    public void initialize(InputSplit split, TaskAttemptContext context)
            throws IOException, InterruptedException {

    }

    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        int offset = 0;
        boolean isKeyValueAvailable = true;
        if (this.start < this.end) {
            offset = this.lineReader.readLine(this.value);
            this.key.set(this.start);
            this.start += offset;
        }

        if (offset == 0) {
            this.key.set(0);
            this.value.set("");
            isKeyValueAvailable = false;
        }
        return isKeyValueAvailable;
    }

    @Override
    public LongWritable getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    @Override
    public Text getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    @Override
    public float getProgress() throws IOException, InterruptedException {
        long splitStart = this.combineFileSplit.getOffset(fileIndex);
        if (this.start < this.end) {
            return Math.min(1.0f, (this.start - splitStart) /
                    (float) (this.end - splitStart));
        }
        return 0;
    }

    @Override
    public void close() throws IOException {
        if (lineReader != null) {
            lineReader.close();
        }
    }
}

CustomPathAndSizeFilter.java(演示过滤器)

/**
 * 过滤器: 文件名需要匹配一个特定的正则表达式, 并满足最小文件大小.
 * <p>
 *     两个要求都有特定的作业参数:
 *     filter.name
 *     filter.min.size
 *     实现时需要扩展Configured类, 并实现PathFilter接口
 * </p>
 * <p>
 *     FileInputFormat.setInputPathFilter(job, CustomPathAndSizeFilter.class);
 * </p>
 *
 * Created by zhangws on 16/10/8.
 */
public class CustomPathAndSizeFilter extends Configured implements PathFilter {

    private Configuration configuration;
    private Pattern filePattern;
    private long filterSize;
    private FileSystem fileSystem;

    @Override
    public boolean accept(Path path) {
        boolean isFileAcceptable = true;
        try {
            if (fileSystem.isDirectory(path)) {
                return true;
            }
            if (filePattern != null) {
                Matcher m = filePattern.matcher(path.toString());
                isFileAcceptable = m.matches();
            }
            if (filterSize > 0) {
                long actualFileSize = fileSystem.getFileStatus(path).getLen();
                if (actualFileSize > this.filterSize) {
                    isFileAcceptable &= true;
                } else {
                    isFileAcceptable = false;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return isFileAcceptable;
    }

    @Override
    public void setConf(Configuration conf) {
        this.configuration = conf;
        if (this.configuration != null) {
            String filterRegex = this.configuration.get("filter.name");
            if (filterRegex != null) {
                this.filePattern = Pattern.compile(filterRegex);
            }

            String filterSizeString = this.configuration.get("filter.min.size");
            if (filterSizeString != null) {
                this.filterSize = Long.parseLong(filterSizeString);
            }
            try {
                this.fileSystem = FileSystem.get(this.configuration);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

CustomCombineFilesDemo.java

public class CustomCombineFilesDemo {

    public static class CombineFilesMapper extends Mapper<LongWritable, Text, LongWritable, Text> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            context.write(key, value);
        }
    }

    public static void main(String[] args)
            throws IOException, InterruptedException, ClassNotFoundException {

        GenericOptionsParser parser = new GenericOptionsParser(args);
        Configuration config = parser.getConfiguration();
        String[] remainingArgs = parser.getRemainingArgs();

        //先删除output目录
        HdfsUtil.rmr(config, remainingArgs[remainingArgs.length - 1]);

        // 实例化任务
        Job job = Job.getInstance(config, CustomCombineFilesDemo.class.getSimpleName());
        // 设置任务类
        job.setJarByClass(CustomCombineFilesDemo.class);

        // 设置Map任务类
        job.setMapperClass(CombineFilesMapper.class);
        // 设置Reduce类(此处设置为没有Reduce任务)
        job.setNumReduceTasks(0);

        // 设置输入格式
        job.setInputFormatClass(CustomCombineFileInputFormat.class);
        // 设置输入过滤器
        FileInputFormat.setInputPathFilter(job, CustomPathAndSizeFilter.class);
        // 设置输入文件或路径
        FileInputFormat.addInputPath(job, new Path(remainingArgs[0]));

        // 设置输出格式
        job.setOutputFormatClass(TextOutputFormat.class);
        // 设置输出目录
        TextOutputFormat.setOutputPath(job, new Path(remainingArgs[1]));
        // 设置输出类型
        job.setOutputKeyClass(LongWritable.class);
        job.setOutputValueClass(Text.class);

        // 启动并等待任务完成
        job.waitForCompletion(true);
    }
}

运行参数

hadoop jar mr-demo-1.0-SNAPSHOT.jar \
com.zw.mr.combine.CustomCombineFilesDemo -D filter.name=.*.txt -D filter.min.size=2014 /hw/hdfs/mr/combine/awd_2003_12 /hw/hdfs/mr/combine/output

结果

13:28:35,188 | INFO  | main             | FileInputFormat                  | reduce.lib.input.FileInputFormat  281 | Total input paths to process : 9
13:28:35,202 | INFO  | main             | CombineFileInputFormat           | lib.input.CombineFileInputFormat  413 | DEBUG: Terminated node allocation with : CompletedNodes: 2, size left: 26112
13:28:35,238 | INFO  | main             | JobSubmitter                     | he.hadoop.mapreduce.JobSubmitter  199 | number of splits:1

6. 参考

《精通Hadoop》 [印] Sandeep Karanth著 刘淼等译

[Hadoop源码详解]之一MapReduce篇之InputFormat

基于Hadoop Sequencefile的小文件解决方案

MR案例:小文件合并SequeceFile

官方HadoopArchives文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值