类图
1. InputFormat类
MapReduce作业的输入数据的规格是通过InputFormat类及其子类给出的。有以下几项主要功能:
- 输入数据的有效性检测。
- 将输入数据切分为逻辑块(InputSplit),并把他们分配给对应的Map任务。
- 实例化一个能在每个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及其派生类有以下几个主要属性:
- 输入文件名。
- 分片数据在文件中的偏移量。
- 分片数据的长度(以字节为单位)。
- 分片数据所在的节点的位置信息。
在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的驱动:
- run()方法首先调用setup()进行初始操作
- 然后循环对每个从context.nextKeyValue()获取的“K-V对”调用map()函数进行处理
- 最后调用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序列化到大文件中。这种文件格式有以下好处:
- 支持压缩,且可定制为基于Record或Block压缩(Block级压缩性能较优)
- 本地化任务支持:因为文件可以被切分,因此MapReduce任务时数据的本地化情况应该是非常好的。
- 难度低:因为是Hadoop框架提供的API,业务逻辑侧的修改比较简单。
坏处是需要一个合并文件的过程,且合并后的文件将不方便查看。
该方案对于小文件的存取都比较自由,不限制用户和文件的多少,但是SequenceFile文件不能追加写入,适用于一次性写入大量小文件的操作。
SequenceFile分别提供了读、写、排序的操作类。SequenceFile的操作中有三种处理方式:
- NONE:不压缩数据直接存储
- RECORD:压缩value值不压缩key值存储的存储方式
- 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著 刘淼等译