Hadoop的初学者经常会疑惑这样两个问题:1.Hadoop的一个Block默认是64M,那么对于一个记录行形式的文本,会不会造成一行记录被分到两个Block当中?2.在把文件从Block中读取出来进行切分时,会不会造成一行记录被分成两个InputSplit,如果被分成两个InputSplit,这样一个InputSplit里面就有一行不完整的数据,那么处理这个InputSplit的Mapper会不会得出不正确的结果?
对于上面的两个问题,首先要明确两个概念:Block和InputSplit
1. block是hdfs存储文件的单位(默认是64M);
2.InputSplit是MapReduce对文件进行处理和运算的输入单位,只是一个逻辑概念,每个InputSplit并没有对文件实际的切割,只是记录了要处理的数据的位置(包括文件的path和hosts)和长度(由start和length决定)。
因此,以行记录形式的文本,还真可能存在一行记录被划分到不同的Block,甚至不同的DataNode上去。通过分析FileInputFormat里面的getSplits方法,可以得出,某一行记录同样也可能被划分到不同的InputSplit。
- publicList<InputSplit>getSplits(JobContextjob)throwsIOException{
- longminSize=Math.max(getFormatMinSplitSize(),getMinSplitSize(job));
- longmaxSize=getMaxSplitSize(job);
-
-
- List<InputSplit>splits=newArrayList<InputSplit>();
- List<FileStatus>files=listStatus(job);
- for(FileStatusfile:files){
- Pathpath=file.getPath();
- longlength=file.getLen();
- if(length!=0){
- FileSystemfs=path.getFileSystem(job.getConfiguration());
- BlockLocation[]blkLocations=fs.getFileBlockLocations(file,0,length);
- if(isSplitable(job,path)){
- longblockSize=file.getBlockSize();
- longsplitSize=computeSplitSize(blockSize,minSize,maxSize);
-
- longbytesRemaining=length;
- while(((double)bytesRemaining)/splitSize>SPLIT_SLOP){
- intblkIndex=getBlockIndex(blkLocations,length-bytesRemaining);
- splits.add(makeSplit(path,length-bytesRemaining,splitSize,
- blkLocations[blkIndex].getHosts()));
- bytesRemaining-=splitSize;
- }
-
- if(bytesRemaining!=0){
- splits.add(makeSplit(path,length-bytesRemaining,bytesRemaining,
- blkLocations[blkLocations.length-1].getHosts()));
- }
- }else{
- splits.add(makeSplit(path,0,length,blkLocations[0].getHosts()));
- }
- }else{
-
- splits.add(makeSplit(path,0,length,newString[0]));
- }
- }
-
- job.getConfiguration().setLong(NUM_INPUT_FILES,files.size());
- LOG.debug("Total#ofsplits:"+splits.size());
- returnsplits;
- }
从上面的代码可以看出,对文件进行切分其实很简单:获取文件在HDFS上的路径和Block信息,然后根据splitSize
对文件进行切分,splitSize = computeSplitSize(blockSize, minSize, maxSize);blockSize,minSize,maxSize都可以配置,默认splitSize 就等于blockSize的默认值(64m)。
FileInputFormat对文件的切分是严格按照偏移量来的,因此一行记录比较长的话,其可能被切分到不同的InputSplit。但这并不会对Map造成影响,尽管一行记录可能被拆分到不同的InputSplit,但是与FileInputFormat关联的RecordReader被设计的足够健壮,当一行记录跨InputSplit时,其能够到读取不同的InputSplit,直到把这一行记录读取完成,在Hadoop里,记录行形式的文本,通常采用默认的TextInputFormat,TextInputFormat关联的是LineRecordReader,下面我们来看看LineRecordReader的的nextKeyValue方法里读取文件的代码:
- while(getFilePosition()<=end){
- newSize=in.readLine(value,maxLineLength,
- Math.max(maxBytesToConsume(pos),maxLineLength));
- if(newSize==0){
- break;
- }
其读取文件是通过LineReader(in就是一个LineReader实例)的readLine方法完成的:
- publicintreadLine(Textstr,intmaxLineLength,
- intmaxBytesToConsume)throwsIOException{
- if(this.recordDelimiterBytes!=null){
- returnreadCustomLine(str,maxLineLength,maxBytesToConsume);
- }else{
- returnreadDefaultLine(str,maxLineLength,maxBytesToConsume);
- }
- }
-
-
-
-
- privateintreadDefaultLine(Textstr,intmaxLineLength,intmaxBytesToConsume)
- throwsIOException{
- str.clear();
- inttxtLength=0;
- intnewlineLength=0;
- booleanprevCharCR=false;
- longbytesConsumed=0;
- do{
- intstartPosn=bufferPosn;
- if(bufferPosn>=bufferLength){
- startPosn=bufferPosn=0;
- if(prevCharCR)
- ++bytesConsumed;
- bufferLength=in.read(buffer);
- if(bufferLength<=0)
- break;
- }
- for(;bufferPosn<bufferLength;++bufferPosn){
- if(buffer[bufferPosn]==LF){
- newlineLength=(prevCharCR)?2:1;
- ++bufferPosn;
- break;
- }
- if(prevCharCR){
- newlineLength=1;
- break;
- }
- prevCharCR=(buffer[bufferPosn]==CR);
- }
- intreadLength=bufferPosn-startPosn;
- if(prevCharCR&&newlineLength==0)
- --readLength;
- bytesConsumed+=readLength;
- intappendLength=readLength-newlineLength;
- if(appendLength>maxLineLength-txtLength){
- appendLength=maxLineLength-txtLength;
- }
- if(appendLength>0){
- str.append(buffer,startPosn,appendLength);
- txtLength+=appendLength;
- }
- }while(newlineLength==0&&bytesConsumed<maxBytesToConsume);<spanstyle="color:#ff0000;">
-
- if(bytesConsumed>(long)Integer.MAX_VALUE)
- thrownewIOException("Toomanybytesbeforenewline:"+bytesConsumed);
- return(int)bytesConsumed;
- }
我们分析下readDefaultLine方法,do-while循环体主要是读取文件,然后遍历读取的内容,找到默认的换行符就终止循环。前面说,对于跨InputSplit的行,LineRecordReader会自动跨InputSplit去读取。这就体现在上述代码的While循环的终止条件上:
while (newlineLength == 0 && bytesConsumed < maxBytesToConsume);
newlineLength==0则以为一次do-while循环中读取的内容中没有遇到换行符,因maxBytesToConsume的默认值为Integer.MAX_VALUE,所以如果读取的内容没有遇到换行符,则会一直读取下去,知道读取的内容超过maxBytesToConsume。这样的出来方式,解决了一行记录跨InputSplit的读取问题,同样也会造成下面两个疑问:
1.既然在LineReader读取方法里面没有对考虑InputSplit的end进行处理,难道读取一个InputSplit的时候,会这样无限的读取下去么?
2.如果一行记录L跨越了A,B两个InputSplit,读A的时候已经读取了跨越A,B的这条记录L,那么对B这个InputSplit读取的时候,如果做到不读取L这条记录在B中的部分呢?
为了解决这两个问题,Hadoop通过下面的代码来做到:LineRecordReader的nextKeyValue方法。
- publicbooleannextKeyValue()throwsIOException{
- if(key==null){
- key=newLongWritable();
- }
- key.set(pos);
- if(value==null){
- value=newText();
- }
- intnewSize=0;
-
-
- while(getFilePosition()<=end){<spanstyle="color:#ff0000;">
- newSize=in.readLine(value,maxLineLength,
- Math.max(maxBytesToConsume(pos),maxLineLength));
- if(newSize==0){
- break;
- }
- pos+=newSize;
- inputByteCounter.increment(newSize);
- if(newSize<maxLineLength){
- break;
- }
-
-
- LOG.info("Skippedlineofsize"+newSize+"atpos"+
- (pos-newSize));
- }
- if(newSize==0){
- key=null;
- value=null;
- returnfalse;
- }else{
- returntrue;
- }
- }
通过代码②处得While条件,就保证了InputSplit读取边界的问题,如果存在跨InputSplit的记录,也只好跨InputSplit读取一次。
再来看LineRecordReader的initialize方法:
-
-
-
- if(start!=0){
- start+=in.readLine(newText(),0,maxBytesToConsume(start));
- }
- this.pos=start;
如果不是第一InputSplit,则在读取的时候,LineRecordReader会自动忽略掉第一个换行符之前的所有内容,这样就不存在重读读取的问题。
此次,前面提到的两个问题就回到完了。。。。。