標籤:hadoop framework 架構 泛型 源碼
本文的主要目的是從源碼層級講解Hadoop中InputFormat和OutputFormat部分,首先簡介InputFormat和OutputFormat,然後介紹兩個重要的組件,RecordWriter和RecordReader,再以FileInputFormat和FileOutputFormat為例,介紹一組InputFormat和OutputFormat的實現細節,最後以SqoopInputFormat和SqoopOutputFormat為例,體會一下InputFormat和OutputFormat的靈活應用。
InputFormat和OutputFormat簡介
InputFormat規定了MR架構,如何解析輸入檔案,比如說TextOutputFormat內部實現了一個LineRecordReader,從LineRecordReader#nextKeyValue方法可以看出,它的key是將要讀的這一行資料的起始位元組位置,value是這一行的內容。
OutputFormat規定了MR架構,最後輸出的檔案的內容格式,比如說SequenceFileOutputFormat,其getRecordWriter方法返回一個RecordWriter的內部類,具體的操作在SequenceFile類裡write相關的方法中。SequenceFile的具體實現只從類來看比價複雜,其一條記錄可以簡單理解成如下的格式[record length][key length][key][value]。
RecordReader和RecordWriter簡介
上邊提到了RecordReader和RecordWriter,這裡簡單介紹一下,RecordWriter和RecordReader在MR架構每一個輸入輸出的地方用到,讀寫操作調用的都是RecordReader和RecordWriter提供的介面,比如說我們在Mapper#map和Reducer#reduce的結束通常都會寫這樣一行代碼context.write(key,value),這行代碼實際調用的就是RecordWriter#write方法,RecordReader是MR架構調用的。
一個InputFormat對應一個RecordReader,一個OutputFormat對應一個RecordWriter。這樣在後邊的DIY的時候,也知道應該寫些什麼了。
FileInputFormat和FileOutputFormat
這裡以FileInputFormat,FileOutputFormat和其對應的子類TextInputFormat和TextOutputFormat為例,分析一套InputFormat和OutputFormat的具體實現。
1. InputFormat
InputFormat抽象類別中有兩個方法:
InputFormat#getSplits(JobContext context)
對輸入檔案進行邏輯上的分區
InputFormat#createRecordReader(InputSplitsplit,TaskAttemptContext context)
返回一個RecordReader對象,該對象可將輸入的InputSplit解析成若干個key/value對。
2. FileInputFormat
所有需要處理檔案的InputFormat的基類,從其實現可以看出,FileInputFormat主要是實現InputFormat#getSplit(JobContext context)方法,由於每個子類對應不同的輸入格式,所以解析InputSplit的方法InputFormat#createRecordReader(InputSplit split,TaskAttemptContextcontext)由各個子類自己實現。
這裡我們先分析FileInputFormat#getSplit(JobContext context)方法。
/** * Generate the list of files and make them into FileSplits. * @param job the job context * @throws IOException */ public List<InputSplit> getSplits(JobContext job) throwsIOException { long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); long maxSize = getMaxSplitSize(job); // generatesplits 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);//擷取一個檔案的所有的Block的帶位置的資訊 if (isSplitable(job, path)) { long blockSize = file.getBlockSize(); long splitSize =computeSplitSize(blockSize, minSize, maxSize);//計算一個FileSplit的大小,計算過程如下,Math.max(minSize,Math.min(maxSize, blockSize)) long bytesRemaining =length; //這個while迴圈就是根據上邊的準備的資訊,不停的讀檔案,產生FileSplit,一直到檔案末尾。 while (((double)bytesRemaining)/splitSize > SPLIT_SLOP) { int blkIndex =getBlockIndex(blkLocations, length-bytesRemaining); splits.add(makeSplit(path,length-bytesRemaining, splitSize, blkLocations[blkIndex].getHosts())); bytesRemaining -= splitSize; } //處理最後不足splitSize大小的資料 if (bytesRemaining != 0) { int blkIndex =getBlockIndex(blkLocations, length-bytesRemaining); splits.add(makeSplit(path,length-bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts())); } } else { // not splitable不能分割的話,檔案只有一塊blkLocations[0]就是這一塊 splits.add(makeSplit(path, 0, length,blkLocations[0].getHosts())); } } else { //Create empty hosts array for zerolength 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; }
3. TextInputFormat
TextInputFormat直接使用了FileInputFormat的getSplits方法,自己實現了createRecordReader方法。
TextInputFormat#createRecordReader方法邏輯比較簡單,最後返回一個LineRecordReader對象,下面主要看這個方法LineRecordReader#nextKeyValue。
public boolean nextKeyValue() throws IOException { if (key == null) { key = newLongWritable(); } key.set(pos); if (value == null) { value = new Text(); } int newSize = 0; // We always read oneextra line, which lies outside the upper // split limit i.e. (end -1)while (getFilePosition() <= end) {//這一句就是讀資料的地方了 newSize =in.readLine(value, maxLineLength, Math.max(maxBytesToConsume(pos), maxLineLength)); if (newSize == 0) { break; } pos += newSize; if (newSize <maxLineLength) { break; } // line too long. tryagain LOG.info("Skippedline of size " + newSize + " at pos " + (pos -newSize)); } if (newSize == 0) { key = null; value = null; return false; } else { return true; } }
4. OutputFormat
OutputFormat# getRecordWriter (TaskAttemptContext context)
返回一個RecordWriter對象
OutputFormat# checkOutputSpecs (JobContext context)
判斷輸出檔案存不存在,寫過MR程式的人應該很熟悉了,如果輸出路徑已經存在話,會拋出一個Output directory " + outDir + " already exists"錯誤,就是這個方法拋出的。
OutputFormat# getOutputCommitter (TaskAttemptContext context)
MR運行過程中,會產生很多中間檔案,比如Mapper的輸出,推測式執行Task時產生的檔案等等,這個方法負責在任務執行完成後,處理這些中間檔案。順便說下,OutputCommitter對象裡的方法都是回調方法,MR自動調用。
5. FileOutputFormat
FileOutputFormat對上邊提到的3個方法中的後兩個提供了通用的實現,OutputFormat# getRecordWriter (TaskAttemptContext context)方法需要子類自己實現。
public void checkOutputSpecs(JobContext job ) throws FileAlreadyExistsException, IOException{ // Ensure that the outputdirectory is set and not already there Path outDir =getOutputPath(job); if (outDir == null) { throw newInvalidJobConfException("Output directory not set."); } // get delegation tokenfor outDir's file system TokenCache.obtainTokensForNamenodes(job.getCredentials(), new Path[] { outDir },job.getConfiguration()); if(outDir.getFileSystem(job.getConfiguration()).exists(outDir)) { throw newFileAlreadyExistsException("Output directory " + outDir + " already exists"); } } public synchronized OutputCommittergetOutputCommitter(TaskAttemptContext context )throws IOException { if (committer == null) { Path output =getOutputPath(context); committer = newFileOutputCommitter(output, context); } return committer; }
6. TextOutputFormat
TextOutputFormat直接使用了FileInputFormat的getSplits方法,自己實現了createRecordReader方法。
TextOutputFormat#createRecordWriter方法邏輯比較簡單,最後返回一個LineRecordWriter對象,下面主要看這個方法LineRecordReader#write(K key,V Value)。
public synchronized void write(K key, V value) throws IOException { boolean nullKey = key ==null || key instanceof NullWritable; boolean nullValue =value == null || value instanceof NullWritable; if (nullKey &&nullValue) { return; } //這個地方是最主要的邏輯 if (!nullKey) { writeObject(key);//寫出key } if (!(nullKey ||nullValue)) { out.write(keyValueSeparator);//寫出key/value的分隔字元 } if (!nullValue) { writeObject(value);//寫出value } out.write(newline);//寫出行結束符}
SqoopInputFormat和SqoopOutputFormat
下面的分析基於Sqoop1.99.3
在Sqoop中,InputFormat和OutputFormat的子類有3個,分別是:
public class SqoopInputFormat extendsInputFormat<SqoopSplit, NullWritable>public class SqoopFileOutputFormat extendsFileOutputFormat<Data, NullWritable>public class SqoopNullOutputFormat extendsOutputFormat<Data, NullWritable>
下面一個個分析:
1. SqoopInputFormat
從泛型傳入的類型可以看出,SqoopInputFormat的key是SqoopSplit,value是NullWritable。直接把整個InputSplit作為key,傳到Mapper中,SqoopInputFormat不對InputSplit做任何的解析操作。
2. SqoopFileOutputFormat
從泛型傳入的類型來看,跟SqoopInputFormat類似,只是用KEY部分儲存資訊。SqoopFileOutputFormat只重寫了父類FileOutputFormat的getRecordWriter方法和getOutputCommitter方法,checkOutputSpec方法使用的父類的。
getRecordWriter方法調用SqoopOutputFormatLoadExecutor#getRecordWriter,這個方法在返回一個SqoopRecordWriter的同時,開啟一個消費者線程,SqoopRecordWriter是生產者線程。[後面接著分析Sqoop源碼時細說]
3. SqoopNullOutputFormat
SqoopNullOutputFormat將OutputFormat的3個方法都重寫了。SqoopNullOutputFormat#getRecordWriter方法同樣是調用SqoopOutputFormatLoadExecutor#getRecordWriter。
InputFormat&OutputFormat