InputFormat:主要用于描述输入数据的格式,它提供了三个功能:
(1)Validate the input-specification of the job.
(2)Split-up the input file(s) into logical {@link InputSplit}s, each of which is then assigned to an individual {@link Mapper}.
(3)Provide the {@link RecordReader} implementation to be used to glean input records from the logical <code>InputSplit</code> for processing by the {@link Mapper}.
(1)为Job验证输入。
(2)数据切分:按照某个策略将输入数据切分成若干个split,以便确定Map Task个数以及对应的split。
(3)为Mapper提供输入数据:给定某个split将其解析成一个个key/value对。
<span style="font-size:18px;">public interface InputFormat<K, V> {
InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
RecordReader<K, V> getRecordReader(InputSplit split,
JobConf job,
Reporter reporter) throws IOException;
}</span>
getSplits方法主要完成数据切分的功能,将输入数据切分成numSplits个InputSplit。
getRecordReader方法返回一个RecordReader对象,该对象可将输入的InputSplit解析成若干个key/value对。
所有基于文件的InputFormat实现的基类是FileInputFormat。整个基于文件的InoutFormat体系的设计思路是,由公共基类FileInputFormat采用统一的方法(文件切分算法)对各种输入文件进行切分,比如按照某个固定大小等分,而由各个派生的InputFormat自己提供机制将进一步解析InputSplit。比如:FileInputFormat继承自InputFormat,但是只是实现了getSplits方法,另一个获取读取器的方法没有实现,这样做是有道理的,因为很多不同格式的文件需要使用不同的读取器来提取数据,比如lzo压缩后的文件的读取器,要先解压后才能读取。
对应到具体的实现是,
(1)基类FileInputFormat提供getSplits实现(主要算法:文件切分算法、hosts选择算法)
(2)而派生类提供getRecordReader实现。
详细讲解下这两步:
(1)关于getSplits方法:
ps:file(文件大小)、block(块大小)、split(分片大小)的关系:
比如设定block=64MB,如果file1=20MB,file2=100MB,则file1有1个块,block1=20MB,file2有2个块,block2=64MB,block3=36MB,分片大小的确定由文件切分算法实现,即分片的大小可能比块的大小大或小。
待InputSplit切分方案确定后,接下来要确定每个InputSplit的元数据信息:InputSplit <file,start,length,hosts>分别表示该InputSplit所在的文件、起始位置、长度、所在的hosts(节点)列表。
确定每个InputSplit的所在hosts列表由hosts选择算法实现。总而言之:当使用FileInputFormat实现InputFormat时,为了提高Map Task的数据本地性,应尽量使InputSplit大小与block大小相同。
源代码中形成splits列表的逻辑大概是这样的:
首先会从 job 对象中所有的输入文件的列表提取出来,List<FileStatus> files = listStatus(job);
然后就要对每个文件进行逻辑分片了,分片的逻辑大概是这样的:首先,计算这个文件的长度(按照字节),然后将这个文件的块信息拿出来。如果这个文件可以被分片并且长度不是0,那么就开始进行逻辑分片。每个分片的大小通过函数computeSplitSize来计算。
然后如果文件的剩余长度是分块的1.1倍以上的话,就创建一个新分片:
splits.add(new FileSplit(path, length-bytesRemaining, splitSize, blkLocations[blkIndex].getHosts()));
进而,将剩余长度减去已经被分配掉的splitSize,这样循环直到不满足条件。
等循环完成之后,如果还有剩余的部分,那么剩下就可以再做一个分片,加入到列表中。
但是,如果我们一开始输入的文件的大小是不可分割的话,那么我们就把整个文件作为一个分片,形成一个实例:
splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
如果这个文件是可分割的,但是长度是0,也做一个默认的分片:
splits.add(new FileSplit(path, 0, length, new String[0]));
文件的分片列表就产生了,然后读取器就可以从这些分片中按照相应的读取逻辑来读取数据,并交给mapper进行处理了。
(2)关于getRecordReader方法:
由FileInputFormat的派生类实现getRecordReader函数,该函数返回一个RecordReader对象,它实现了类似于迭代器的功能,将某个InputSplit解析成一个个kv对,具体实现时应考虑以下两点:
a. 定位边界记录:比如对于TextInpuTFormat,每两条记录之间存在换行符。注意的是,由于FileInputFormat仅仅按照数据量对文件进行切分,因而InputSplit的第一条记录和最后一条记录可能会被中间切开,为了解决这种记录跨越InputSplit读取问题,RecordReader规定每个InputSplit的第一条不完整记录给前一个InputSplit处理。
b. 用什么规则解析成kv对:比如对于TextInputFormat,每一行的内容即为value,而该行在整个文件中的偏移量为key。