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。
public List<InputSplit> getSplits(JobContext job) throws IOException { long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); 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(); long length = file.getLen(); if (length != 0) { FileSystem fs = path.getFileSystem(job.getConfiguration()); BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length); if (isSplitable(job, path)) { long blockSize = file.getBlockSize(); long splitSize = computeSplitSize(blockSize, minSize, maxSize); long bytesRemaining = length; while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { int blkIndex = 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 { // not splitable splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts())); } } 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()); LOG.debug("Total # of splits: " + splits.size()); return splits; }
從上面的代碼可以看出,對檔案進行切分其實很簡單:擷取檔案在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方法完成的:
public int readLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException { if (this.recordDelimiterBytes != null) { return readCustomLine(str, maxLineLength, maxBytesToConsume); } else { return readDefaultLine(str, maxLineLength, maxBytesToConsume); } } /** * Read a line terminated by one of CR, LF, or CRLF. */ private int readDefaultLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException { str.clear(); int txtLength = 0; //tracks str.getLength(), as an optimization int newlineLength = 0; //length of terminating newline boolean prevCharCR = false; //true of prev char was CR long bytesConsumed = 0; do { int startPosn = bufferPosn; //starting from where we left off the last time if (bufferPosn >= bufferLength) { startPosn = bufferPosn = 0; if (prevCharCR) ++bytesConsumed; //account for CR from previous read bufferLength = in.read(buffer); if (bufferLength <= 0) break; // EOF } for (; bufferPosn < bufferLength; ++bufferPosn) { //search for newline if (buffer[bufferPosn] == LF) { newlineLength = (prevCharCR) ? 2 : 1; ++bufferPosn; // at next invocation proceed from following byte break; } if (prevCharCR) { //CR + notLF, we are at notLF newlineLength = 1; break; } prevCharCR = (buffer[bufferPosn] == CR); } int readLength = bufferPosn - startPosn; if (prevCharCR && newlineLength == 0) --readLength; //CR at the end of the buffer bytesConsumed += readLength; int appendLength = readLength - newlineLength; if (appendLength > maxLineLength - txtLength) { appendLength = maxLineLength - txtLength; } if (appendLength > 0) { str.append(buffer, startPosn, appendLength); txtLength += appendLength; } } while (newlineLength == 0 && bytesConsumed < maxBytesToConsume); <span style="color: #ff0000;">//①</span> if (bytesConsumed > (long)Integer.MAX_VALUE) throw new IOException("Too many bytes before newline: " + 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方法。
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) { <span style="color: #ff0000;"> //②</span> newSize = in.readLine(value, maxLineLength, Math.max(maxBytesToConsume(pos), maxLineLength)); if (newSize == 0) { break; } pos += newSize; inputByteCounter.increment(newSize); if (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; } }
通過代碼②處得While條件,就保證了InputSplit讀取邊界的問題,如果存在跨InputSplit的記錄,也只好跨InputSplit讀取一次。
再來看LineRecordReader的initialize方法:
// If this is not the first split, we always throw away first record // because we always (except the last split) read one extra line in // next() method. if (start != 0) { start += in.readLine(new Text(), 0, maxBytesToConsume(start)); } this.pos = start;
如果不是第一InputSplit,則在讀取的時候,LineRecordReader會自動忽略掉第一個分行符號之前的所有內容,這樣就不存在重讀讀取的問題。