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的。