文章目录
一、InputFormat抽象类
InputFormat
主要用于描述输入数据的格式(这里我们只分析新API,即org.apache.hadoop.mapreduce.lib.input.InputFormat
),提供以下两个功能:
- 数据切分:按照某个策略将输入数据切分成若干个split,以便确定
MapTask
个数以及对应的split; - 为
Mapper
提供输入数据:读取给定的split的数据,解析成一个个的key/value
对,供Mapper
使用。
这里我们主要分析最常用的文本文件的读取方式,即TextInputFormat.java
,各个类的继承关系如下所示:
InputFormat
抽象类有两个比较重要的方法:
- 用户获取数据分片的
getSplists
方法:
List<InputSplit> getSplits(JobContext job)
- 读取给定的split数据,生成相应的key/value对的
createRecordReader
方法:
RecordReader<LongWritable, Text> createRecordReader(InputSplit split,TaskAttemptContext context)
二、如何将数据切分成split
2.1 FileInputFormat.java的getSplits()方法
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
//设定的split最小值(mapreduce.input.fileinputformat.split.minsize)
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
//设定的split最大值(mapreduce.input.fileinputformat.split.maxsize)
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
//获取指定路径下文件信息
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
//获取文件路径
Path path = file.getPath();
//文件大小(注意文件大小会超过block大小时会被分成多个block)
long length = file.getLen();
if (length != 0) {
//block数组信息
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
//文件在本地
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
//文件在hdfs
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
//判断文件是否支持文件切割(一般情况下为true,但是部分压缩文件不支持切割)
if (isSplitable(job, path)) {
//支持文件切割
long blockSize = file.getBlockSize();//获取文件的块大小(默认128*1024*1024)
long splitSize = computeSplitSize(blockSize, minSize, maxSize);//获取split大小
//切分split是对整个文件进行切分
//将剩余未分片大小设置成整个文件大小
long bytesRemaining = length;
//只有剩余文件文件大小/分片大小>1.1的时候才进行文件切分
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//获取文件的block索引
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 {
//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());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}
如上代码中使用到了File、Block以及Split,他们之间的关系如下图所示(图中未画出block的副本):
如上的getSplits()
的代码可以使用如下流程图来表示:
2.2 File、Block、Splite
-
block和split的关系
在进行文件切分的时候,我们首先会读取block块的大小以及设置的split大小,接着通过一系列的判断来确定最终的split分片大小(minSplitSize
默认为1b,blockSize
默认为128*1024*1024b):minSize = max{minSplitSize,mapred.min.split.size} maxSize = mapred.max.split.size splitSize = max{minSize,min{maxSize,blockSize}}
-
BlockLocation
一般file超过指定的block的size就会将文件切分成多个块,所以在获得文件块的地址时,会返回一个BlockLocation[]
数组,BlockLocation包含了block的大小,在file中的偏移量以及位置信息等,具体如下属性所示。属性中数组表示block存在多个副本,每个副本的信息都会存在BlockLocation中(详情可以看一下后面的testBlockInfo
的demo)。private String[] hosts; // Datanode hostnames private String[] cachedHosts; // Datanode hostnames with a cached replica private String[] names; // Datanode IP:xferPort for accessing the block private String[] topologyPaths; // Full path name in network topology private long offset; // Offset of the block in the file private long length; private boolean corrupt;
-
InputSplit
getSplits()
归根节点是要文件切分成List<InputSplit> splits
的列表,然后交给MapTask
们去处理,一个MapTask需要处理内容即为每一个InputSplit
,而InputSplit
并不是物理的文件切片,只是对需要处理的文件根据InputSplit
大小进行逻辑上的切分,InputSplit
的实现类为FileSplit
,FileSplit
包含的信息如下所示:private Path file;//文件路径 private long start;//在文件中的偏移量 private long length;//分片大小 private String[] hosts;//block所在的dataNode的hosts private SplitLocationInfo[] hostInfos;//对应hosts,并标记是否在内存中缓存
-
file和block的对应关系
当file的size的大小大于block的大小时,file会被切分成多个block,具体信息如下代码示例:@Test public void testBlockInfo() throws IOException { Configuration conf = new Configuration(); Path myPath = new Path("/test/hadoop-2.7.2.tar.gz"); FileSystem hdfs=myPath.getFileSystem(conf); FileStatus fileStatus=hdfs.getFileStatus(myPath); System.out.println("file info:"); System.out.println("---file size:"+fileStatus.getLen()); System.out.println("---file replication:"+fileStatus.getReplication()); System.out.println("---block of file size:"+fileStatus.getBlockSize()); BlockLocation[] blockLocations=hdfs.getFileBlockLocations(fileStatus,0,fileStatus.getLen()); System.out.println("the block info of file:"); //指定文件被切分的block System.out.println("---total block num:" + blockLocations.length); for (int i=0;i<blockLocations.length;i++){ System.out.println("---block index:"+i); //block所在dataNode名称 System.out.println("------block names :"+ Lists.newArrayList(blockLocations[i].getNames())); //block所在dataNode的hosts System.out.println("------block hosts :"+Lists.newArrayList(blockLocations[i].getHosts())); System.out.println("------block cachedHosts :"+Lists.newArrayList(blockLocations[i].getCachedHosts())); //block的网络拓扑 System.out.println("------block topologyPaths :"+Lists.newArrayList(blockLocations[i].getTopologyPaths())); //block文件大小 System.out.println("------block length :"+Lists.newArrayList(blockLocations[i].getLength())); //block的偏移量 System.out.println("------block offset :"+Lists.newArrayList(blockLocations[i].getOffset())); } }
打印的信息如下所示:
三、如何读取Split为Mapper提供输入文件
3.1 TextInputFormat.java的createRecordReader方法
该方法返回一个RecordReader<LongWritable,Text>
对象,实现类似于迭代器的功能,将文件中的每一行数据转换成key(行起始位置在file中的偏移量)/value(一行数据)的格式。其主要方法如下所示:
TextInputFormat.java
的createRecordReader()
方法读取一个split以及task上下文信息,并返回的是一个LineRecordReader
对象(RecordReader
的子类)来读取split每一行的数据,具体代码如下所示:
@Override
public RecordReader<LongWritable, Text>
createRecordReader(InputSplit split, TaskAttemptContext context) {
String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter)
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
return new LineRecordReader(recordDelimiterBytes);
}
- initialize方法–LineRecordReader.java
在LineRecordReader
类中有一个initialize
方法,该方法用于初始化,主要用于获得split相应的输入流,并且设置split分片在file中的起始位置(start/end
)。
/**
* 主要是打开输入文件获得相应的流(构建一个LineReader对象来负责读取文件内容)并且设置start/end标识
* @param genericSplit
* @param context the information about the task
* @throws IOException
*/
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();//split的开始位置
end = start + split.getLength();//split的结束位置
final Path file = split.getPath();//split所属文件位置
// open the file and seek to the start of the split
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);
//获得可分片压缩文件的LineReader
in = new CompressedSplitLineReader(cIn, job,
this.recordDelimiterBytes);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else {
//获得不可分片的压缩文件的LineReader
in = new SplitLineReader(codec.createInputStream(fileIn,
decompressor), job, this.recordDelimiterBytes);
filePosition = fileIn;
}
} else {
//获得非压缩文件的LineReader
fileIn.seek(start);
in = new UncompressedSplitLineReader(
fileIn, job, this.recordDelimiterBytes, split.getLength());
filePosition = fileIn;
}
/**注意:
* 如果读取的不是第一个split,那么会丢弃第一行数据,因为对于每一个split的最后一行数据会读取一个完整的行(可能会跨split读取)
* 所以如果不是第一个split,会把每个split中的第一条不完整行记录丢弃(因为已经读过了)
* */
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}
上面代码需要注意的是start的起始位移的设置,一开始start=split.getStart();
,后续如果判断当前split不是初试split则会重置start
的位置,为什么会这样,会在后续3.2中介绍。
- nextKeyValue方法–LineRecordReader.java
该方法主要用于一行行读取文件具体的内容,key是split相对于file的偏移量,而value则是具体的行数据。
/**
* 获得key以及value的值,key为当前行在文件中的偏移量,value为行数据
*
* @return
* @throws IOException
*/
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()) {
//while循环中的代码保证了跨split的行读取的完整性
if (pos == 0) {
newSize = skipUtfByteOrderMark();
} else {
//读取文件中的行数据,并复制给value
newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
pos += newSize;
}
if ((newSize == 0) || (newSize < maxLineLength)) {
break;
}
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
这里面有一个需要注意的地方就是,存在file中的一行数据跨split的情况,这就要求在读取split中最后一行数据的时候,会读取下一个split中其起始行的数据,具体情况会在后续的3.2中介绍。
在之前的initialize
方法中,我们根据文件类型的不同生成了三种不同的LineReader
,而在nextKeyValue
方法中我们会根据不同的文件类型的输入流的调用不同的readLine
方法,不同类型的LineReader
的类图如下所示,即:
SplitLineReader
:处理可以切割的数据CompressedSplitLineReader
:处理压缩数据UnCompressedSplitLineReader
:处理不可压缩的数据
而归根结底最终都会去调用LineReader
类中的readLine
方法,下面我们就来看一下readLine
的方法实现逻辑:
public int readLine(Text str, int maxLineLength,
int maxBytesToConsume) throws IOException {
if (this.recordDelimiterBytes != null) {
//使用自定义的分隔符读取行数据
return readCustomLine(str, maxLineLength, maxBytesToConsume);
} else {
//使用默认的分隔符(\r\n)读取行数据
return readDefaultLine(str, maxLineLength, maxBytesToConsume);
}
}
3.2 行跨split的读取方式
LineRecordReader.java
类中initialize
方法展示了如何进行行跨split
的读取方式,当读取split
的最后一行数据的时候,我们会将下一个split
的第一个不完整行给一起读取,接着在读取后面的每一个split
的时候,第一个不完整行会被丢弃,直接从第二行开始读取。如下入所示:
在读取block第三行数据的时候,split1只有该行数据的前半部分,所以为了数据的完整性,会进行跨split进去读取数据,即读取完整的hello world3。那么在读取split2的时候,由于world3已经读取过了,所以丢弃split2中的第一行数据。读取split2时,需要从hello world4开始读取。
3.3 split的跨block的读取方式
实际中当split
的大小分配不恰当的时候,会造成一个split
跨block
(可以参考之前介绍的block和split的关系
)。我们前面已经讲过split
是逻辑分片,不是物理分片,当MapTask
的数据本地性发挥作用时,会从本机的block
开始读取,超过这个block
的部分可能还在本机也可能不在本机,如果是后者的话就要从别的节点拉数据过来,因为实际获取数据是一个输入流,这个输入流面向的是整个文件,不受block和split的影响,split
的大小越大可能需要从别的节点拉的数据越多,从从而效率也会越慢,拉数据的多少是由getSplits
方法中的splitSize
决定的(如下图所示,在处理完split1
中的block1
之后,还需要在其它的datanode2
上去获取block2
中的部分数据)。所以为了更有效率,在设置splitSize
的时候应该尽量将splitSize
的大小设置成blockSize
的大小。