Hadoop源碼分析筆記(九):數據節點--數據塊存儲

數據節點簡介

        我們繼續來了解Hadoop分佈式文件系統各個實體內部的工作原理。首先是數據節點,它以數據塊的形式在本地Linux文件系統上保存HDFS文件的內容,並對外提供文件數據訪問功能。客戶端讀寫HDFS文件時,必須根據名字節點提供的信息,進一步和數據節點交互;同時,數據節點還必須接受名字節點的管理,執行名字節點指令,並上報名字節點感興趣的事件,以保證文件系統穩定、可靠、高效地運行。

數據塊存儲

        HDFS採用了一種非常典型的文件系統實現方法。對一個不定長度的文件進行分塊,並將塊保存在存儲設備上。文件數據分塊,保存在HDFS的存儲設備--數據節點上。數據節點將數據塊以Linux文件的形式保存在節點的存儲系統上。數據節點的第一個基本功能,就是管理這些保存在Linux文件系統中的數據塊。
        在HDFS中,名字節點、數據節點和第二名字節點都需要在磁盤上組織、存儲持久化數據,它們會在磁盤上維護一定的文件結構。在第一次啓動HDFS集羣前,需要通過hadoop namenode -format對名字節點進行格式化,讓名字節點建立對應的文件結構。
       由於需要在HDFS集羣運行時動態添加或刪除數據節點。所以,數據節點和名字節點不一樣,它們不需要進行格式化,而是在第一次啓動的時候,創建存儲目錄。另外,數據節點可以管理多個數據目錄,被管理的目錄通過配置項${dfs.data.dir}指定,如果該配置項的值爲“/data/datanode,/data2/datanode”,則數據節點會管理這兩個目錄,並把它們作爲數據塊保存目錄。

${dfs.data.dir}目錄介紹

         ${dfs.data.dir}目錄下一般有4個目錄和2個文件。(目錄:blocksBeginWriten、current、detach、tmp,文件:in_use.lock、storage)
         其中各個目錄的作用如下:
         blocksBeginWritten:由名字可以知道,該文件夾保存着當前正在“寫”的數據塊。和位於“tmp”目錄下保存的正在“寫”的數據塊相比,差別在於“blocksBeginWritten”中的數據塊寫操作由客戶端發起。
        current:數據節點管理的最重要目錄,它保存着已經寫入HDFS文件系統的數據塊,也就是寫操作已經結束的已“提交”數據塊。該目錄還包含一些系統工作時需要的文件。
        detach:用戶配合數據節點升級,供數據塊分離操作保存臨時工作文件。
         tmp:該文件夾也保存着當前正在“寫”的數據塊,這裏的寫操作時由數據塊複製引發的,另一個數據節點正在發送數據到數據塊。
        上述目錄其實也隱含了數據塊在數據節點上可能存在的狀態,以上述目錄名作爲狀態。
         數據節點上的數據塊最開始會處於“blocksBeingWriteen”(由客戶端寫入創建)或“tmp”(由數據塊複製寫入創建)狀態,當數據寫入順利結束後,提交數據塊,它們的狀態會遷移到“current”。已經提交的數據塊可以重新被打開,追加數據,這時,它的狀態會放回到“blocksBeginWritten”。處於上述的三個狀態的數據塊都可以被刪除,並轉移到“最終狀態”。
狀態“detach”比較特殊,它用於配合數據節點升級,是數據塊可能存在的一個臨時、短暫的狀態。
          ${dfs.data.dir}目錄下還有兩個文件,其中,“storage”保存着如下一段提示信息:
protected void writeCorruptedData(RandomAccessFile file) throws IOException {
    final String messageForPreUpgradeVersion =
      "\nThis file is INTENTIONALLY CORRUPTED so that versions\n"
      + "of Hadoop prior to 0.13 (which are incompatible\n"
      + "with this directory layout) will fail to start.\n";
  
    file.seek(0);
    file.writeInt(FSConstants.LAYOUT_VERSION);
    org.apache.hadoop.io.UTF8.writeString(file, "");
    file.writeBytes(messageForPreUpgradeVersion);
    file.getFD().sync();
  }

         該文件的最開始保存着一個二進制整數,它是一個二進制文件,不是文本文件。由這段信息可知,0.13版本以前的Hadoop,使用“storage”作爲數據塊的保存目錄,和現在的目錄結構不兼容。這是一種值得學習的技巧,可以防止過舊的Hadoop版本在新的目錄結構上成功啓動,損壞系統。
         ${dfs.data.dir}目錄下的另一個文件是”in_use.lock“,它表明目錄已經被使用,實現了一種”鎖“機制。如果停止數據節點,該文件會消失,通過文件”in_use.lock“,數據節點可以保證獨自佔用該目錄,防止兩個數據節點(當然,可以在一個節點上啓動歸屬不同HDFS集羣的多個數據節點)實例共享一個目錄,造成混亂。

${dfs.data.dir}/current目錄

        current目錄是${dfs.data.dir}下唯一帶目錄結構的子目錄,其他三個(blocksBeingWritten、detach和tmp)都沒有子目錄。
        ”current“中既包含目錄,也包含文件。其中,大部分文件都以blk_作爲前綴,這些文件有兩種類型:
        1、 HDFS數據塊,用來保存HDFS文件的內容,如”blk_1221212122“。
        2、使用meta後綴標識的校驗信息文件,用來保存數據塊的校驗信息。
        和ChecksumFileSystem文件系統不同,數據節點中,數據塊的校驗信息文件不是隱藏文件,但它們的文件內容格式是一樣的,都由包含校驗配置信息的文件頭與一系列校驗信息組成。圖中,文件”blk_1222“對應的校驗信息文件時”blk_1222“。
        當目錄中存儲的數據塊增加到一定規模時,數據節點會創建一個新的目錄,用於保存新的塊及元數據。目錄塊中的塊數據達到64時,(由配置項${dfs.datanode.numblocks}指定),便會創建子目錄(如:subdir56),並形成一個更寬的目錄樹結構。同時,同一個父目錄下最多會創建64個子目錄,也就是說,默認配置下,一個目錄下最多隻有64個數據塊(128個文件)和64個目錄。通過這兩個手段,數據節點既保證了目錄的深度不會太深,影響了檢索文件的性能,同時避免了某個目錄保存了大量數據塊,確保每個目錄中的文件塊數是可控的。
        ${dfs.data.dir}/current目錄中還有三個特殊的文件,”VERSION“文件是一個Java屬性文件,包含了運行的HDFS版本信息。另外兩個”dncp_block_verification.log.curr“和”dncp_block_verification.log.prev“,則是數據塊掃描器工作時需要的文件。

  數據節點存儲的實現

        根據軟件開發的一般原則,數據節點的業務邏輯不會在數據節點的文件結構上直接操作,當需要對磁盤上的數據進行操作時,業務邏輯只需要調用管理這些文件結構的對象提供的服務即可。數據節點的文件結構管理包括兩部分內容,數據(節點)存儲DataStorage和文件系統數據集FSDataset。
        HDFS的服務器實體,名字節點、數據節點和第二名字節點都需要使用磁盤保存數據。所以,在org.apache.hadoop.hdfs.server.commom包中定義了一些基礎類,他們抽象了磁盤上的數據組織。
        數據節點存儲DataStorage是抽象類Storage的子類,而抽象類Storage又繼承自StorageInfo。在這個繼承體系中,和DataStorage同級的FSImage類用於組織名字節點的磁盤數據。FSImage的子類CheckpointStorage和FSImage的繼承關係,體現了名字節點和第二名字節點的密切聯繫。
        這個繼承結構中的另外兩個類:NamespaceInfo和CheckpointSignature,出現在HDFS節點間的通信的遠程接口中,它們分別應用於數據節點和名字節點的通信的DatanodeProtocol接口和第二名字節點和名字節點通信的NamenodeProtocol接口。NamespaceInfo包含HDFS集羣存儲的一些信息,CheckpointSignature對象,則用於標識名字節點元數據的檢查點。另外,StorageInfo包含的三個字段的含義,分別是HDFS存儲系統信息結構的版本號layoutVersion、存儲系統標識namespaceID和存儲系統創建時間cTime。在${dfs.data.dir}/current目錄中,文件”VERSION“保存着這些信息。
         “VERSION”是一個典型的Java屬性文件,數據節點的“VERSION”文件除了上面提到的三個屬性外,還有storageType和strorangeID屬性。
         storageType屬性的意義是一目瞭然,它可以表明這是一個數據節點數據存儲目錄(如:storageType=DATA_NODE);storageID用於名字節點標識數據節點,一個集羣的不同數據節點擁有不同的storageID。
       storageID屬性保存在DataStorage類中,如下所示:
/** 
 * Data storage information file.
 * <p>
 * @see Storage
 */
public class DataStorage extends Storage {
  // Constants
  final static String BLOCK_SUBDIR_PREFIX = "subdir";
  final static String BLOCK_FILE_PREFIX = "blk_";
  final static String COPY_FILE_PREFIX = "dncp_";
  
  private String storageID;
  ......
}

        StorageInfo是一個非常簡單的類,包含三個屬性和相應的get/set方法。它的抽象子類Storage也定義在org.apache.hadoop.hdfs.server,common包中,爲數據節點、名字節點等提供通用的存儲服務。Storage可以管理多個目錄,存儲目錄StorageDirectory是Storage的內部類。提供了存儲目錄上的一些通用操作,它們的實現都很簡單,值得分析的是StorageDirectory的成員變量和getVersionFile()、tryLock()等方法。
        成員變量StorageDirectory.root保存着存儲目錄的根,dirType保存着該目錄對應的類型。需要注意的是類型爲java.nio.channels.FileLock的成員變量lock。FileLock,就是文件鎖。Java的文件鎖,要麼獨佔,那麼共享。在這裏,StorageDirectory使用的是獨佔文件鎖,對lock的加鎖代碼在tryLock()中。
       數據節點的文件結構中,當數據節點運行時,${dfs.data.dir}下會有一個名爲“in_use.lock”的文件,就是由tryLock()方法創建並上鎖的。注意,“創建並上鎖”兩個操作缺一不可,如果只是創建文件但不加鎖,不能防止用戶對文件的誤操作,如刪除文件或移動文件造成“in_use.lock”文件丟失,導致StorageDirectory.tryLock()判斷邏輯失效。通過java.nio.channels.FileChannel.tryLock()方法,StorageDirectory的tryLock()方法獲得了文件的獨佔鎖定,可以避免上述問題,並通過數據節點的實現邏輯,保證對StorageDirectory對象指向目錄的獨佔使用。同時,“in_use.lock”文件會在數據節點退出時刪除,對應的實現代碼是lockF.deleteOnExit();
         deleteOnExit()方法時由java.io.File類提供,當虛擬機退出時,調用了該方法的文件會被虛擬機自動刪除。當然,如果tryLock()加鎖失敗,deleteOnExit()方法自然也不會其作用。相關代碼如下:
/**
   * One of the storage directories.
   */
  public class StorageDirectory {
    File              root; // root directory
    FileLock          lock; // storage lock
    StorageDirType dirType; // storage dir type
    ....... 
/**
     * Attempts to acquire an exclusive lock on the storage.
     * 
     * @return A lock object representing the newly-acquired lock or
     * <code>null</code> if storage is already locked.
     * @throws IOException if locking fails.
     */
    FileLock tryLock() throws IOException {
      File lockF = new File(root, STORAGE_FILE_LOCK);
      lockF.deleteOnExit();
      RandomAccessFile file = new RandomAccessFile(lockF, "rws");
      FileLock res = null;
      try {
        res = file.getChannel().tryLock();
      } catch(OverlappingFileLockException oe) {
        file.close();
        return null;
      } catch(IOException e) {
        LOG.error("Cannot create lock on " + lockF, e);
        file.close();
        throw e;
      }
      return res;
    }
   ......
}

         StorageDirectory還提供了一系列的get/set方法,如獲取存儲目錄中的“VERSION”文件的File對象,可以調用getVersionFile()方法。StorageDirectory.analyzeStorage()和StorageDirectory.doRecover()涉及系統升級。
         Storage管理着一個或多個StorageDirectory對象,所有Storage類中的方法都很簡單,或者是在StorageDirectory基礎提供的整體操作,或者是對保存的StorageDirectory對象進行管理。Storage是個抽象類,有兩個抽象方法,它們和系統升級相關。
         和Storage一樣,DataStorage的代碼可以分爲兩部分,升級相關代碼和升級無關代碼。與升級無關的代碼很少,需要關注的是DataStorage定義的三個常量,對照數據節點的文件結構。代碼如下:
        
public class DataStorage extends Storage {
  // Constants
  final static String BLOCK_SUBDIR_PREFIX = "subdir";
  final static String BLOCK_FILE_PREFIX = "blk_";
  final static String COPY_FILE_PREFIX = "dncp_";
  
  private String storageID;

  DataStorage() {
    super(NodeType.DATA_NODE);
    storageID = "";
  }

...... 
void format(StorageDirectory sd, NamespaceInfo nsInfo) throws IOException {
    sd.clearDirectory(); // create directory
    this.layoutVersion = FSConstants.LAYOUT_VERSION;
    this.namespaceID = nsInfo.getNamespaceID();
    this.cTime = 0;
    // store storageID as it currently is
    sd.write();
  }
}
       數據節點在第一次啓動的時候,會調用DataStorage.format()創建存儲目錄結果。
       format()方法很簡單,通過StorageDirectory.clearDirectory()刪除原有目錄及數據並重新創建目錄,然後“VERSION”文件中的屬性賦值並將其持久化到磁盤。format()的第一個參數類型是StorageDirectory,如果數據節點管理這多個目錄,這個方法會被調用多次,在不同的目錄下創建節點文件結構;第二個參數nsInfo是從名字節點返回的NamespaceInfo對象,攜帶了存儲系統標識namespaceID等信息,該標識最終存放在“VERSION”文件中。注意,由於“VERSION”文件保存在current目錄中,保存文件的同時會創建對應的目錄。

 數據節點升級

        上面介紹將DataStorage的功能分爲升級相關和升級無關兩個部分有點勉強,其實,DataStorage的主要功能是對存儲空間的生存期進行管理,通過DataStorage.format()創建存儲目錄,或者利用DataStoarage.doUpgrade()進行升級,都是存儲空間生存期管理的一部分。對於數據節點,存儲空間生存期管理的關注點還是系統升級。
         Hadoop實現了嚴格的版本兼容性要求,只有來自相同版本的組件才能保證相互的兼容性。大型分佈式系統的升級需要一個周密的計劃,必須考慮到諸如:持久化的信息是否改變?如果數據佈局改變,如何自動地將原有數據和元數據遷移到新版本格式?升級過程中如果出現錯誤,如何保證數據不丟失?升級出現問題時,怎麼才能夠回滾升級前的狀態?
        如果文件系統的佈局不需要改變,集羣的升級變得非常簡單,關閉舊進程,升級配置文件,然後啓動新版本的守護進程,客戶端使用新的庫,就可以完成升級。同時,升級回滾也很簡單,使用舊版本和舊配置文件,重啓系統即可。如果持久化的信息在上述過程中需要改變到新的格式,特別是考慮到可能的回滾,升級的過程就變得複雜。
       HDFS升級時,需要複製以前版本的元數據(對名字節點)和數據(對數據節點)。在數據節點上,升級並不需要兩倍的集羣存儲空間,數據節點使用了Linux文件系統的硬鏈接,保留了對同一個數據塊的兩個引用,即當前版本和以前版本。通過這樣的技術,就可以在需要的時候,輕鬆回滾到以前版本的文件系統。注意,在已升級系統上對數據的修改,升級回滾結束後將會消失。
      1、升級
       爲了簡化實現,HDFS最多保留前一版本的文件系統,沒有多版本的升級、回滾機制。同時,引入升級提交機制(通過hadoop dfsadmin-finalizeUpgrade可提交一次升級),該機制用於刪除以前的版本,所以在升級提交後,就不能回滾到以前的版本了。
        數據節點升級的實現在DataStorage.doUpgrade()方法中。其中,升級過程涉及如下幾個目錄:
        curDir:當前版本目錄,通過StorageDirectory.getCurrentDir()獲得,目錄名爲“current”。
        prevDir:上以版本目錄,目錄名爲"previous",可通過StorageDirectory的getPreviousDir()方法得到;這裏的“上一版本”指的是升級前的版本。
        tmpDir:上一個版本臨時目錄,即目錄${dfs.data.dir}/previous.tmp
        doUpgrade()方法的主要流程是:首先確保上述涉及的工作目錄處於正常狀態。如檢查目錄“current”目錄是否存在,如果“previous”目錄存在,刪除該目錄。注意,該刪除操作相當於提交了上一次升級,同時保證HDFS最多保留前一段版本的要求;保證“tmpDir”目錄不存在。應該說,在(通過硬鏈接)移動數據前,“previous”目錄和“previous.tmp”都是不存在的。
        支持升級回滾,就必須保留升級前的數據,在數據節點,就是保存數據塊以及數據塊的校驗信息文件。在doUpgrade()中,保留升級前數據是通過建立文件的硬鏈接實現的。
         DataStorage.doUpgrade()保留升級前數據的動作一共有兩步:首先將“current”目錄名改爲“previous.tmp”,然後調用linkBlocks(),在新創建的“current”目錄下,建立到“pervious.tmp”目錄中數據塊和數據塊校驗信息文件的硬鏈接。DataStorage.lineBlocks()執行結束後,doUpgrade()方法還需要在“current”目錄下創建這版本的“VERSION”文件,最後,將“previous.tmp”目錄改爲"previous",完成升級。這個時候,數據節點的存儲空間會有"previous"和“current”兩個目錄,而且,“previous”和“current”包含了同樣的數據塊和數據塊校驗信息文件,但它們有各自“VERSION”文件。另外,升級過程中需要的“previous.tmp”目錄已經消失。代碼如下:
        
/**
   * Move current storage into a backup directory,
   * and hardlink all its blocks into the new current directory.
   * 
   * @param sd  storage directory
   * @throws IOException
   */
  void doUpgrade(StorageDirectory sd,
                 NamespaceInfo nsInfo
                 ) throws IOException {
    LOG.info("Upgrading storage directory " + sd.getRoot()
             + ".\n   old LV = " + this.getLayoutVersion()
             + "; old CTime = " + this.getCTime()
             + ".\n   new LV = " + nsInfo.getLayoutVersion()
             + "; new CTime = " + nsInfo.getCTime());
    // enable hardlink stats via hardLink object instance
    HardLink hardLink = new HardLink();
    
    File curDir = sd.getCurrentDir();
    File prevDir = sd.getPreviousDir();
    assert curDir.exists() : "Current directory must exist.";
    // delete previous dir before upgrading
    if (prevDir.exists())
      deleteDir(prevDir);
    File tmpDir = sd.getPreviousTmp();
    assert !tmpDir.exists() : "previous.tmp directory must not exist.";
    // rename current to tmp
    rename(curDir, tmpDir);
    // hardlink blocks
    linkBlocks(tmpDir, curDir, this.getLayoutVersion(), hardLink);
    // write version file
    this.layoutVersion = FSConstants.LAYOUT_VERSION;
    assert this.namespaceID == nsInfo.getNamespaceID() :
      "Data-node and name-node layout versions must be the same.";
    this.cTime = nsInfo.getCTime();
    sd.write();
    // rename tmp to previous
    rename(tmpDir, prevDir);
    LOG.info( hardLink.linkStats.report());
    LOG.info("Upgrade of " + sd.getRoot()+ " is complete.");
  }
        2、升級回滾
        升級回滾doRollBack()的實現有如下要點:
         1)在完成對各個工作目錄的狀態的檢查後,需要保證升級能夠回滾到正確的版本上去。這個步驟是通過比較保存着在“previous”目錄下“VERSION”文件中的HDFS存儲系統信息結構的版本號layoutVersion和存儲系統創建時間cTime和回滾後相應的layoutVersion和cTime來做判斷。
        2)由於“pervious”目錄保存了升級前的所有數據,所以,doRollback()其實只需要簡單的將“previous”目錄改名稱“current”,就可以完成回滾。但由於現在版本的工作目錄“current”目錄存在,所有采用了三步操作完成改名動作:先將“current”目錄改爲“removed.tmp”,然後將“previous”目錄名修改爲“current”,最後刪除“removed.tmp”目錄。代碼如下:
void doRollback( StorageDirectory sd,
                   NamespaceInfo nsInfo
                   ) throws IOException {
    File prevDir = sd.getPreviousDir();
    // regular startup if previous dir does not exist
    if (!prevDir.exists())
      return;
    DataStorage prevInfo = new DataStorage();
    StorageDirectory prevSD = prevInfo.new StorageDirectory(sd.getRoot());
    prevSD.read(prevSD.getPreviousVersionFile());

    // We allow rollback to a state, which is either consistent with
    // the namespace state or can be further upgraded to it.
    if (!(prevInfo.getLayoutVersion() >= FSConstants.LAYOUT_VERSION
          && prevInfo.getCTime() <= nsInfo.getCTime()))  // cannot rollback
      throw new InconsistentFSStateException(prevSD.getRoot(),
                                             "Cannot rollback to a newer state.\nDatanode previous state: LV = " 
                                             + prevInfo.getLayoutVersion() + " CTime = " + prevInfo.getCTime() 
                                             + " is newer than the namespace state: LV = "
                                             + nsInfo.getLayoutVersion() + " CTime = " + nsInfo.getCTime());
    LOG.info("Rolling back storage directory " + sd.getRoot()
             + ".\n   target LV = " + nsInfo.getLayoutVersion()
             + "; target CTime = " + nsInfo.getCTime());
    File tmpDir = sd.getRemovedTmp();
    assert !tmpDir.exists() : "removed.tmp directory must not exist.";
    // rename current to tmp
    File curDir = sd.getCurrentDir();
    assert curDir.exists() : "Current directory must exist.";
    rename(curDir, tmpDir);
    // rename previous to current
    rename(prevDir, curDir);
    // delete tmp dir
    deleteDir(tmpDir);
    LOG.info("Rollback of " + sd.getRoot() + " is complete.");
  }

         3、升級提交
         升級提交doFinalize()簡單更簡單,它只需要將“previous”目錄刪除即可。但實際上,DataStorage.doFinalize()需要將“previous”目錄名改爲“finalized.tmp”,然後刪除“finalized.tmp”來刪除目錄,而不是直接刪除“previous”來提交升級。代碼如下:
        
void doFinalize(StorageDirectory sd) throws IOException {
    File prevDir = sd.getPreviousDir();
    if (!prevDir.exists())
      return; // already discarded
    final String dataDirPath = sd.getRoot().getCanonicalPath();
    LOG.info("Finalizing upgrade for storage directory " 
             + dataDirPath 
             + ".\n   cur LV = " + this.getLayoutVersion()
             + "; cur CTime = " + this.getCTime());
    assert sd.getCurrentDir().exists() : "Current directory must exist.";
    final File tmpDir = sd.getFinalizedTmp();
    // rename previous to tmp
    rename(prevDir, tmpDir);

    // delete tmp dir in a separate thread
    new Daemon(new Runnable() {
        public void run() {
          try {
            deleteDir(tmpDir);
          } catch(IOException ex) {
            LOG.error("Finalize upgrade for " + dataDirPath + " failed.", ex);
          }
          LOG.info("Finalize upgrade for " + dataDirPath + " is complete.");
        }
        public String toString() { return "Finalize " + dataDirPath; }
      }).start();
  }
        也許讀者會有疑問,爲什麼在doFinalize()中不能直接刪除“previous”目錄?同樣,在doRollback()和doUpgrade()中,都存在“removed.tmp”和“previous.tmp”這樣的臨時目錄。
        在升級、升級提交或升級回滾都需要進行一定的操作。在執行這些任務的中間,如果設備出現故障(如:停電),那麼,存儲空間就可能處於某一箇中間狀態。引入上述這些臨時目錄,系統就能夠判斷目前doUpgrade()處於什麼階段,並採取相應的應對措施。
         Storage和DataStorage一起提供了一個完美的HDFS數據節點升級機制,簡化了大型分佈式系統的運維。它們不但解決了升級過程中數據格式轉換、升級回滾等常見問題、並保證了這個過程中不會丟失數據;同時,它們的設計,特別是Storage狀態機,以及配合狀態機工作的臨時文件,提供了一個完備的升級方法,在升級過程或者回滾過程中的任意一個步驟出現錯誤,都可以通過狀態機,恢復到正常狀態。HDFS數據節點升級的Storage和DataStorage實現,非常具有借鑑意義。

       文件系統數據集的工作機制 

         DataStorage專注於數據節點的存儲空間的生存期管理,在存儲空間上,與數據節點邏輯密切相關的存儲服務,如創建數據塊文件、維護數據塊文件和數據塊校驗信息文件的關係等,則實現在文件系統數據集FSDataset中。 FSDataset繼承了FSDatasetInterface接口。
        FSDatasetInterface接口的方法可以分爲以下三類:
        數據塊相關的方法:FSDataset管理了數據節點上的數據塊,大量的FSDataset方法和數據塊相關,如創建數據塊、打開數據塊上的輸入、輸出流、提交數據塊等。
        數據塊校驗信息文件相關:包括維護數據塊和校驗信息文件關係,獲取校驗信息文件輸入流等。
       其他:包括FSDataset健康檢查、關閉FSDataset的shutdown()方法等。
        FSDataset借鑑了Linux邏輯卷管理的一些思想。LVM使系統管理員可以更方便地分配存儲空間,它的出現,改變了磁盤空間的管理理念。傳統的文件系統是基於分區的,一個文件系統對應一個分期,這種方式直觀,但不靈活,在實際應用中,會出現分去不平衡,存儲空間和硬件能力得不到充分利用等問題。
        FSDataset沒有使用Linx LVM的方式管理它的存儲空間,但是FSDataset借鑑了LVM的一些概念,可以管理多個數據目錄。一般情況下,這些不同的數據目錄配置在不同的物理設備上,以提高磁盤的數據吞吐量。
        文件數據集將它管理的存儲空間分爲三個級別,分別是FSDir、FSVolume和FSVolumeSet進行抽象。
        FSDir對象表示了“current”目錄下的子目錄,其成員變量children包括了目錄下的所有子目錄,形成了目錄樹。FSVolume是數據目錄配置項${dfs.data.dir}中的一項,數據節點可以管理一個或者多個數據目錄,系統中也就存在一個或者多個FSVolum對象,這些FSVolume對象由FSVolumSet管理。
         由於上述內部類存在,FSDataset的成員變量不多,比較重要的有volumes,它管理數據節點的所有存儲空間。成員ongoingCreates和VolomeMap是兩個HaseMap,其中,ongoingCreates保存着當前正在進行寫操作的數據塊和對應文件的影射,volumeMap則保存着已經提交的數據塊和對應的文件影射。代碼如下:
/**************************************************
 * FSDataset manages a set of data blocks.  Each block
 * has a unique name and an extent on disk.
 *
 ***************************************************/
public class FSDataset implements FSConstants, FSDatasetInterface {
  

  /** Find the metadata file for the specified block file.

/**
   * Start writing to a block file
   * If isRecovery is true and the block pre-exists, then we kill all
      volumeMap.put(b, v);
      volumeMap.put(b, v);
   * other threads that might be writing to this block, and then reopen the file.
   * If replicationRequest is true, then this operation is part of a block
   * replication request.
   */
  public BlockWriteStreams writeToBlock(Block b, boolean isRecovery,
                           boolean replicationRequest) throws IOException {
    //
    // Make sure the block isn't a valid one - we're still creating it!
    //
    if (isValidBlock(b)) {
      if (!isRecovery) {
        throw new BlockAlreadyExistsException("Block " + b + " is valid, and cannot be written to.");
      }
      // If the block was successfully finalized because all packets
      // were successfully processed at the Datanode but the ack for
      // some of the packets were not received by the client. The client 
      // re-opens the connection and retries sending those packets.
      // The other reason is that an "append" is occurring to this block.
      detachBlock(b, 1);
    }
    long blockSize = b.getNumBytes();

    //
    // Serialize access to /tmp, and check if file already there.
    //
    File f = null;
    List<Thread> threads = null;
    synchronized (this) {
      //
      // Is it already in the create process?
      //
      ActiveFile activeFile = ongoingCreates.get(b);
      if (activeFile != null) {
        f = activeFile.file;
        threads = activeFile.threads;
        
        if (!isRecovery) {
          throw new BlockAlreadyExistsException("Block " + b +
                                  " has already been started (though not completed), and thus cannot be created.");
        } else {
          for (Thread thread:threads) {
            thread.interrupt();
          }
        }
        ongoingCreates.remove(b);
      }
      FSVolume v = null;
      if (!isRecovery) {
        v = volumes.getNextVolume(blockSize);
        // create temporary file to hold block in the designated volume
        f = createTmpFile(v, b, replicationRequest);
      } else if (f != null) {
        DataNode.LOG.info("Reopen already-open Block for append " + b);
        // create or reuse temporary file to hold block in the designated volume
        v = volumeMap.get(b).getVolume();
        volumeMap.put(b, new DatanodeBlockInfo(v, f));
      } else {
        // reopening block for appending to it.
        DataNode.LOG.info("Reopen Block for append " + b);
        v = volumeMap.get(b).getVolume();
        f = createTmpFile(v, b, replicationRequest);
        File blkfile = getBlockFile(b);
        File oldmeta = getMetaFile(b);
        File newmeta = getMetaFile(f, b);

        // rename meta file to tmp directory
        DataNode.LOG.debug("Renaming " + oldmeta + " to " + newmeta);
        if (!oldmeta.renameTo(newmeta)) {
          throw new IOException("Block " + b + " reopen failed. " +
                                " Unable to move meta file  " + oldmeta +
                                " to tmp dir " + newmeta);
        }

        // rename block file to tmp directory
        DataNode.LOG.debug("Renaming " + blkfile + " to " + f);
        if (!blkfile.renameTo(f)) {
          if (!f.delete()) {
            throw new IOException("Block " + b + " reopen failed. " +
                                  " Unable to remove file " + f);
          }
          if (!blkfile.renameTo(f)) {
            throw new IOException("Block " + b + " reopen failed. " +
                                  " Unable to move block file " + blkfile +
                                  " to tmp dir " + f);
          }
        }
      }
      if (f == null) {
        DataNode.LOG.warn("Block " + b + " reopen failed " +
                          " Unable to locate tmp file.");
        throw new IOException("Block " + b + " reopen failed " +
                              " Unable to locate tmp file.");
      }
      // If this is a replication request, then this is not a permanent
      // block yet, it could get removed if the datanode restarts. If this
      // is a write or append request, then it is a valid block.
      if (replicationRequest) {
        volumeMap.put(b, new DatanodeBlockInfo(v));
      } else {
        volumeMap.put(b, new DatanodeBlockInfo(v, f));
      }
      ongoingCreates.put(b, new ActiveFile(f, threads));
    }

    try {
      if (threads != null) {
        for (Thread thread:threads) {
          thread.join();
        }
      }
    } catch (InterruptedException e) {
      throw new IOException("Recovery waiting for thread interrupted.");
    }

    //
    // Finally, allow a writer to the block file
    // REMIND - mjc - make this a filter stream that enforces a max
    // block size, so clients can't go crazy
    //
    File metafile = getMetaFile(f, b);
    DataNode.LOG.debug("writeTo blockfile is " + f + " of size " + f.length());
    DataNode.LOG.debug("writeTo metafile is " + metafile + " of size " + metafile.length());
    return createBlockWriteStreams( f , metafile);
  }

  synchronized File createTmpFile( FSVolume vol, Block blk,
                        boolean replicationRequest) throws IOException {
    if ( vol == null ) {
      vol = volumeMap.get( blk ).getVolume();
      if ( vol == null ) {
        throw new IOException("Could not find volume for block " + blk);
      }
    }
    return vol.createTmpFile(blk, replicationRequest);
  }

 private synchronized void finalizeBlockInternal(Block b, boolean reFinalizeOk) 
    throws IOException {
    ActiveFile activeFile = ongoingCreates.get(b);
    if (activeFile == null) {
      if (reFinalizeOk) {
        return;
      } else {
        throw new IOException("Block " + b + " is already finalized.");
      }
    }
    File f = activeFile.file;
    if (f == null || !f.exists()) {
      throw new IOException("No temporary file " + f + " for block " + b);
    }
    FSVolume v = volumeMap.get(b).getVolume();
    if (v == null) {
      throw new IOException("No volume for temporary file " + f + 
                            " for block " + b);
    }
        
    File dest = null;
    dest = v.addBlock(b, f);
    volumeMap.put(b, new DatanodeBlockInfo(v, dest));
    ongoingCreates.remove(b);
  }
}

打開數據塊和提交數據塊的方法如上代碼所示。

版權申明:本文部分摘自【蔡斌、陳湘萍】所著【Hadoop技術內幕 深入解析Hadoop Common和HDFS架構設計與實現原理】一書,僅作爲學習筆記,用於技術交流,其商業版權由原作者保留,推薦大家購買圖書研究,轉載請保留原作者,謝謝!
      
發佈了1 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章