源码:
public abstract class InputFormat<K, V> {
public InputFormat() {
}
public abstract List<InputSplit> getSplits(JobContext var1) throws IOException, InterruptedException;
public abstract RecordReader<K, V> createRecordReader(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
}
-向Hadoop集群提交作业时,需要指定作业的输入的格式(未指认时默认的输入格式为TextInputFormat)。
-旧的APT(hadoop-0.x)中InputFormat是被定义为接口,新的API(hadoop-1.x及hadoop-2.x)中InputFormat是做为抽象类存在的
-InputFormat主要用于验证作业的输入是否符合规范,将输入的文件分割为逻辑InputSplit,每个inputSplit被分配给一个Mapper任务,提供RecordReader的实现,该实现负责从InputSplit中收集记录交给由Mapper任务处理。
-不同的InputFormat子类提供了不同的InputSplit和RecordReader。
-InputFormat抽象类只提供了两个抽象类方法,分别用于获取InputSplit和RecordReader。
-InputSplit只是对输入文件的逻辑分割,而不是物理上将输入文件分割成块,或者说InputSplit只是指定了输入文件的某个范围输入到特定的Mapper中。
-List getSplits(), 获取由输入文件计算出输入分片(InputSplit),解决数据或文件分割成片问题。
-RecordReader
-验证作业输入的正确性
-将输入文件切割成逻辑分片(InputSplit),一个InputSplit将会被分配给一个独立的MapTask
-提供RecordReader实现,读取InputSplit中的“K-V对”供Mapper使用
源码:
public abstract class InputSplit {
public 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;
}
}
-Mappers的输入是一个一个的输入分片,称InputSplit。
-nputSplit也是一个抽象类,它在逻辑上包含了提供给处理这个InputSplit的Mapper的所有K-V对。
-InputSplit的子类FileSplit类:FileSplit有四个属性:文件路径,分片起始位置,分片长度和存储分片的hosts。用这四项数据,就可以计算出提供给每个Mapper的分片数据。在InputFormat的getSplit()方法中构造分片,分片的四个属性会通过调用FileSplit的Constructor设置。
-(如果用FileSplit对应的FileInputFormat作为输入格式,那么即使文件特别小,也是作为一个单独的InputSplit来处理,而每一个InputSplit将会由一个独立的Mapper Task来处理。在输入数据是由大量小文件组成的情形下,就会有同样大量的InputSplit,从而需要同样大量的Mapper来处理,大量的Mapper Task创建销毁开销将是巨大的,甚至对集群来说,是灾难性的!)
-InputSplit的子类CombineFileSplit:CombineFileSplit是针对小文件的分片,它将一系列小文件封装在一个InputSplit内,这样一个Mapper就可以处理多个小文件。可以有效的降低进程开销。与FileSplit类似,CombineFileSplit同样包含文件路径,分片起始位置,分片大小和分片数据所在的host列表四个属性,只不过这些属性不再是一个值,而是一个列表。需要注意的一点是,CombineFileSplit的getLength()方法,返回的是这一系列数据的数据的总长度。
源码
public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable {
public RecordReader() {
}
//初始化RecordReader。
public abstract void initialize(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
//RecordReader中最主要的方法,由它获取分片上的下一个K-V对。
public abstract boolean nextKeyValue() throws IOException, InterruptedException;
//获取当前的Key。
public abstract KEYIN getCurrentKey() throws IOException, InterruptedException;
//获取当前的Value。
public abstract VALUEIN getCurrentValue() throws IOException, InterruptedException;
//记录RecordReader当前通过其数据的进展。
public abstract float getProgress() throws IOException, InterruptedException;
//关闭RecordReader。
public abstract void close() throws IOException;
}
public List<InputSplit> getSplits(JobContext job) throws IOException {
//开启线程
Stopwatch sw = (new Stopwatch()).start();-
//首先计算分片的最大和最小值。这两个值将会用来计算分片的大小。
long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
//splits链表用来存储计算得到的输入分片结果
List<InputSplit> splits = new ArrayList();
//files链表存储由listStatus()获取的输入文件列表,listStatus比较特殊
List<FileStatus> files = this.listStatus(job);
Iterator i$ = files.iterator();
while(true) {
while(true) {
while(i$.hasNext()) {
FileStatus file = (FileStatus)i$.next();
Path path = file.getPath();
long length = file.getLen(); //整个文件的长度
//判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割
if (length != 0L) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus)file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
//获取该文件所有的block信息列表[hostname, offset, length]
blkLocations = fs.getFileBlockLocations(file, 0L, length);
}
//是否分割可以自行重写FileInputFormat的isSplitable来控制
if (this.isSplitable(job, path)) {
long blockSize = file.getBlockSize();
//计算分片大小
//即 Math.max(minSize, Math.min(maxSize, blockSize));
//也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize
long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining;
int blkIndex;
//循环分片。当剩余数据与分片大小比值大于1.1D时,继续分片, 小于等于时,停止分片
for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
}
//处理余下的数据
if (bytesRemaining != 0L) {
blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
splits.add(this.makeSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
}
} else {
//不可split,整块返回
splits.add(this.makeSplit(path, 0L, length, blkLocations[0].getHosts(), blkLocations[0].getCachedHosts()));
}
} else {
//对于长度为0的文件,创建空Hosts列表,返回
splits.add(this.makeSplit(path, 0L, length, new String[0]));
}
}
//设置输入文件数量
job.getConfiguration().setLong("mapreduce.input.fileinputformat.numinputfiles", (long)files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size() + ", TimeTaken: " + sw.elapsedMillis());
}
return splits;
}
}
}
-FileInputFormat继承InputFormat
-实现了getSplits()方法,getSplits()方法将文件切分成InputSplit,InputSplit的个数对应map()方法的个数。
源码:
public class TextInputFormat extends FileInputFormat<LongWritable, Text> {
public TextInputFormat() {
}
public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");//设置record的分隔符
byte[] recordDelimiterBytes = null;
if (null != delimiter) {
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
}
return new LineRecordReader(recordDelimiterBytes);
}
protected boolean isSplitable(JobContext context, Path file) {
CompressionCodec codec = (new CompressionCodecFactory(context.getConfiguration())).getCodec(file);
return null == codec ? true : codec instanceof SplittableCompressionCodec;
}
}
-TextInputFormat继承 FileInputFormat抽象类
-实现了createRecordReader()方法,createRecordReader()将InputSplit切分成可供map()处理的
-换行符实际上是由”textinputformat.record.delimiter”这个配置决定的
-getSplits是在逻辑上划分,并没有物理切分,也就是只是记录每个split从文件的个位置读到哪个位置,文件还是一个整体。所以在LineRecordReader中,它的处理方式是每个split多读一行,也就是读到下一个split的第一行。然后除了每个文件的第一个split,其他split都跳过第一行,进而避免重复读取,这种方式去处理。
源码
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Mapper() {
}
//预处理,仅在map task启动时运行一次
protected void setup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
}
//对于InputSplit中的每一对都会运行一次
protected void map(KEYIN key, VALUEIN value, Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
context.write(key, value);
}
//扫尾工作,比如关闭流等
protected void cleanup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
}
//map task的驱动器
public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKeyValue()) {
this.map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
this.cleanup(context);
}
}
public abstract class Context implements MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Context() {
}
}
}
-run()方法首先调用setup()进行初始操作
-然后循环对每个从context.nextKeyValue()获取的“K-V对”调用map()函数进行处理
-最后调用cleanup()做最后的处理