MapReduce InputFormat之FileInputFormat

Thinkgamer_gyt發表於2015-11-30

一:簡單認識InputFormat類

InputFormat主要用於描述輸入資料的格式,提供了以下兩個功能: 

        1)、資料切分,按照某個策略將輸入資料且分成若干個split,以便確定Map Task的個數即Mapper的個數,在MapReduce框架中,一個split就意味著需要一個Map Task; 

        2)為Mapper提供輸入資料,即給定一個split,(使用其中的RecordReader物件)將之解析為一個個的key/value鍵值對。

下面我們先來看以下1.0版本中的老的InputFormat介面:

Java程式碼 
  1. public interface InputFormat<K,V>{  
  2.      
  3.    //獲取所有的split分片     
  4.    public InputSplit[] getSplits(JobConf job,int numSplits) throws IOException;   
  5.     
  6.    //獲取讀取split的RecordReader物件,實際上是由RecordReader物件將  
  7.    //split解析成一個個的key/value對兒  
  8.    public RecordReader<K,V> getRecordReader(InputSplit split,  
  9.                                JobConf job,  
  10.                                Reporter reporter) throws IOException;   
  11. }  
InputSplit 
        getSplit(...)方法主要用於切分資料,它會嘗試浙江輸入資料且分成numSplits個InputSplit的栓皮櫟split分片。InputSplit主要有以下特點: 
        1)、邏輯分片,之前我們已經學習過split和block的對應關係和區別,split只是在邏輯上對資料分片,並不會在磁碟上講資料切分成split物理分片,實際上資料在HDFS上還是以block為基本單位來儲存資料的。InputSplit只記錄了Mapper要處理的資料的後設資料資訊,如起始位置、長度和所在的節點; 


  2)、可序列化,在Hadoop中,序列化主要起兩個作用,程式間通訊和資料持久化儲存。在這裡,InputSplit主要用於程式間的通訊。 
         在作業被提交到JobTracker之前,Client會先呼叫作業InputSplit中的getSplit()方法,並將得到的分片資訊序列化到檔案中,這樣,在作業在JobTracker端初始化時,便可並解析出所有split分片,建立物件應的Map Task。 
         InputSplit也是一個interface,具體返回什麼樣的implement,這是由具體的InputFormat來決定的。InputSplit也只有兩個介面函式:

Java程式碼 
  1. public interface InputSplit extends Writable {  
  2.   
  3.   /** 
  4.    * 獲取split分片的長度 
  5.    *  
  6.    * @return the number of bytes in the input split. 
  7.    * @throws IOException 
  8.    */  
  9.   long getLength() throws IOException;  
  10.     
  11.   /** 
  12.    * 獲取存放這個Split的Location資訊(也就是這個Split在HDFS上存放的機器。它可能有 
  13.    * 多個replication,存在於多臺機器上 
  14.    *  
  15.    * @return list of hostnames where data of the <code>InputSplit</code> is 
  16.    *         located as an array of <code>String</code>s. 
  17.    * @throws IOException 
  18.    */  
  19.   String[] getLocations() throws IOException;  
  20. }  
 在需要讀取一個Split的時候,其對應的InputSplit會被傳遞到InputFormat的第二個介面函式getRecordReader,然後被用於初始化一個RecordReader,以便解析輸入資料,描述Split的重要資訊都被隱藏了,只有具體的InputFormat自己知道,InputFormat只需要保證getSplits返回的InputSplit和getRecordReader所關心的InputSplit是同樣的implement就行了,這給InputFormat的實現提供了巨大的靈活性。 
         在MapReduce框架中最常用的FileInputFormat為例,其內部使用的就是FileSplit來描述InputSplit。我們來看一下FileSplit的一些定義資訊:
Java程式碼  
  1. /** A section of an input file.  Returned by {@link 
  2.  * InputFormat#getSplits(JobConf, int)} and passed to 
  3.  * {@link InputFormat#getRecordReader(InputSplit,JobConf,Reporter)}.  
  4.  */  
  5. public class FileSplit extends org.apache.hadoop.mapreduce.InputSplit   
  6.                        implements InputSplit {  
  7.   // Split所在的檔案  
  8.   private Path file;  
  9.   // Split的起始位置  
  10.   private long start;  
  11.   // Split的長度  
  12.   private long length;  
  13.   // Split所在的機器名稱  
  14.   private String[] hosts;  
  15.     
  16.   FileSplit() {}  
  17.   
  18.   /** Constructs a split. 
  19.    * @deprecated 
  20.    * @param file the file name 
  21.    * @param start the position of the first byte in the file to process 
  22.    * @param length the number of bytes in the file to process 
  23.    */  
  24.   @Deprecated  
  25.   public FileSplit(Path file, long start, long length, JobConf conf) {  
  26.     this(file, start, length, (String[])null);  
  27.   }  
  28.   
  29.   /** Constructs a split with host information 
  30.    * 
  31.    * @param file the file name 
  32.    * @param start the position of the first byte in the file to process 
  33.    * @param length the number of bytes in the file to process 
  34.    * @param hosts the list of hosts containing the block, possibly null 
  35.    */  
  36.   public FileSplit(Path file, long start, long length, String[] hosts) {  
  37.     this.file = file;  
  38.     this.start = start;  
  39.     this.length = length;  
  40.     this.hosts = hosts;  
  41.   }  
  42.   
  43.   /** The file containing this split's data. */  
  44.   public Path getPath() { return file; }  
  45.     
  46.   /** The position of the first byte in the file to process. */  
  47.   public long getStart() { return start; }  
  48.     
  49.   /** The number of bytes in the file to process. */  
  50.   public long getLength() { return length; }  
  51.   
  52.   public String toString() { return file + ":" + start + "+" + length; }  
  53.   
  54.   ////////////////////////////////////////////  
  55.   // Writable methods  
  56.   ////////////////////////////////////////////  
  57.   
  58.   public void write(DataOutput out) throws IOException {  
  59.     UTF8.writeString(out, file.toString());  
  60.     out.writeLong(start);  
  61.     out.writeLong(length);  
  62.   }  
  63.   public void readFields(DataInput in) throws IOException {  
  64.     file = new Path(UTF8.readString(in));  
  65.     start = in.readLong();  
  66.     length = in.readLong();  
  67.     hosts = null;  
  68.   }  
  69.   
  70.   public String[] getLocations() throws IOException {  
  71.     if (this.hosts == null) {  
  72.       return new String[]{};  
  73.     } else {  
  74.       return this.hosts;  
  75.     }  
  76.   }  
  77.     
  78. }  

         從上面的程式碼中我們可以看到,FileSplit就是InputSplit介面的一個實現。InputFormat使用的RecordReader將從FileSplit中獲取資訊,解析FileSplit物件從而獲得需要的資料的起始位置、長度和節點位置。 

  RecordReader 
         對於getRecordReader(...)方法,它返回一個RecordReader物件,該物件可以講輸入的split分片解析成一個個的key/value對兒。在Map Task的執行過程中,會不停的呼叫RecordReader物件的方法,迭代獲取key/value並交給map()方法處理:

Java程式碼 
  1. //呼叫InputFormat的getRecordReader()獲取RecordReader<K,V>物件,  
  2. //並由RecordReader物件解析其中的input(split)...  
  3. K1 key = input.createKey();  
  4. V1 value = input.createValue();  
  5. while(input.next(key,value)){//從input讀取下一個key/value對  
  6.     //呼叫使用者編寫的map()方法  
  7. }  
  8. input.close();  

         RecordReader主要有兩個功能: 
         ●定位記錄的邊界:由於FileInputFormat是按照資料量對檔案進行切分,因而有可能會將一條完整的記錄切成2部分,分別屬於兩個split分片,為了解決跨InputSplit分片讀取資料的問題,RecordReader規定每個分片的第一條不完整的記錄劃給前一個分片處理。 
         ●解析key/value:定位一條新的記錄,將記錄分解成key和value兩部分供Mapper處理。 

InputFormat 
         MapReduce自帶了一些InputFormat的實現類: 


 下面我們看幾個有代表性的InputFormat: 
         FileInputFormat 
         FileInputFormat是一個抽象類,它最重要的功能是為各種InputFormat提供統一的getSplits()方法,該方法最核心的是檔案切分演算法和Host選擇演算法:

Java程式碼 
  1. /** Splits files returned by {@link #listStatus(JobConf)} when 
  2.    * they're too big.*/   
  3. @SuppressWarnings("deprecation")  
  4. public InputSplit[] getSplits(JobConf job, int numSplits)  
  5.     throws IOException {  
  6.     FileStatus[] files = listStatus(job);  
  7.       
  8.     // Save the number of input files in the job-conf  
  9.     job.setLong(NUM_INPUT_FILES, files.length);  
  10.     long totalSize = 0;                           // compute total size  
  11.     for (FileStatus file: files) {                // check we have valid files  
  12.       if (file.isDir()) {  
  13.         throw new IOException("Not a file: "+ file.getPath());  
  14.       }  
  15.       totalSize += file.getLen();  
  16.     }  
  17.       
  18.     long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);  
  19.     long minSize = Math.max(job.getLong("mapred.min.split.size"1),  
  20.                             minSplitSize);  
  21.   
  22.     // 定義要生成的splits(FileSplit)的集合  
  23.     ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);  
  24.     NetworkTopology clusterMap = new NetworkTopology();  
  25.     for (FileStatus file: files) {  
  26.       Path path = file.getPath();  
  27.       FileSystem fs = path.getFileSystem(job);  
  28.       long length = file.getLen();  
  29.       BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);  
  30.       if ((length != 0) && isSplitable(fs, path)) {   
  31.         long blockSize = file.getBlockSize();  
  32.         //獲取最終的split分片的大小,該值很可能和blockSize不相等  
  33.         long splitSize = computeSplitSize(goalSize, minSize, blockSize);  
  34.   
  35.         long bytesRemaining = length;  
  36.         while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {  
  37.           //獲取split分片所在的host的節點資訊  
  38.           String[] splitHosts = getSplitHosts(blkLocations,   
  39.               length-bytesRemaining, splitSize, clusterMap);  
  40.           //最終生成所有分片  
  41.           splits.add(new FileSplit(path, length-bytesRemaining, splitSize,   
  42.               splitHosts));  
  43.           bytesRemaining -= splitSize;  
  44.         }  
  45.           
  46.         if (bytesRemaining != 0) {  
  47.           splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,   
  48.                      blkLocations[blkLocations.length-1].getHosts()));  
  49.         }  
  50.       } else if (length != 0) {  
  51.         //獲取split分片所在的host的節點資訊  
  52.         String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);  
  53.         //最終生成所有分片  
  54.         splits.add(new FileSplit(path, 0, length, splitHosts));  
  55.       } else {   
  56.         //Create empty hosts array for zero length files  
  57.         //最終生成所有分片  
  58.         splits.add(new FileSplit(path, 0, length, new String[0]));  
  59.       }  
  60.     }  
  61.     LOG.debug("Total # of splits: " + splits.size());  
  62.     return splits.toArray(new FileSplit[splits.size()]);  
  63. }  

          1)、檔案切分演算法 
          檔案切分演算法主要用於確定InputSplit的個數以及每個InputSplit對應的資料段,FileInputSplit以檔案為單位切分生成InputSplit。有三個屬性值來確定InputSplit的個數: 
          ●goalSize:該值由totalSize/numSplits來確定InputSplit的長度,它是根據使用者的期望的InputSplit個數計算出來的;numSplits為使用者設定的Map Task的個數,預設為1。 
          ●minSize:由配置引數mapred.min.split.size決定的InputFormat的最小長度,預設為1。 
          ●blockSize:HDFS中的檔案儲存塊block的大小,預設為64MB。 
          這三個引數決定一個InputFormat分片的最終的長度,計算方法如下: 
                      splitSize = max{minSize,min{goalSize,blockSize}} 
計算出了分片的長度後,也就確定了InputFormat的數目。 

          2)、host選擇演算法 
          InputFormat的切分方案確定後,接下來就是要確定每一個InputSplit的後設資料資訊。InputSplit後設資料通常包括四部分,<file,start,length,hosts>其意義為: 
          ●file標識InputSplit分片所在的檔案; 
          ●InputSplit分片在檔案中的的起始位置; 
          ●InputSplit分片的長度; 
          ●分片所在的host節點的列表。 
          InputSplit的host列表的算作策略直接影響到執行作業的本地性。我們知道,由於大檔案儲存在HDFS上的block可能會遍佈整個Hadoop叢集,而一個InputSplit分片的劃分演算法可能會導致一個split分片對應多個不在同一個節點上的blocks,這就會使得在Map Task執行過程中會涉及到讀其他節點上的屬於該Task的block中的資料,從而不能實現資料本地性,而造成更多的網路傳輸開銷。 
          一個InputSplit分片對應的blocks可能位於多個資料節點地上,但是基於任務排程的效率,通常情況下,不會把一個分片涉及的所有的節點資訊都加到其host列表中,而是選擇包含該分片的資料總量的最大的前幾個節點,作為任務排程時判斷是否具有本地性的主要憑證。 
         FileInputFormat使用了一個啟發式的host選擇演算法:首先按照rack機架包含的資料量對rack排序,然後再在rack內部按照每個node節點包含的資料量對node排序,最後選取前N個(N為block的副本數)node的host作為InputSplit分片的host列表。當任務地排程Task作業時,只要將Task排程給host列表上的節點,就可以認為該Task滿足了本地性。 
         從上面的資訊我們可以知道,當InputSplit分片的大小大於block的大小時,Map Task並不能完全滿足資料的本地性,總有一本分的資料要通過網路從遠端節點上讀資料,故為了提高Map Task的資料本地性,減少網路傳輸的開銷,應儘量是InputFormat的大小和HDFS的block塊大小相同。 

          TextInputFormat 
          預設情況下,MapReduce使用的是TextInputFormat來讀分片並將記錄資料解析成一個個的key/value對,其中key為該行在整個檔案(注意而不是在一個block)中的偏移量,而行的內容即為value。 
          CombineFileInputFormat 
          CombineFileInputFormat的作用是把許多檔案合併作為一個map的輸入,它的主要思路是把輸入目錄下的大檔案分成多個map的輸入, 併合並小檔案, 做為一個map的輸入。適合在處理多個小檔案的場景。 
          SequenceFileInputFormat 
          SequenceFileInputFormat是一個順序的二進位制的FileInputFormat,內部以key/value的格式儲存資料,通常會結合LZO或Snappy壓縮演算法來讀取或儲存可分片的資料檔案。

相關文章