源碼|HDFS之NameNode:建立目錄

namenode主要負責文件元信息的管理和文件到數據塊的映射。其中,建立目錄只涉及文件元信息的操做。本文分析namenode建立目錄過程的源碼實現,爲後面分析寫文件過程打基礎。java

源碼版本:Apache Hadoop 2.6.0node

可參考猴子追源碼時的速記打斷點,親自debug一遍。git

開始以前

總覽

根據HDFS-1.x、2.x的RPC接口源碼|HDFS之NameNode:啓動過程,咱們得知,與建立目錄過程聯繫最緊密的是ClientProtocol協議、RpcServer線程、FSNamesystem、FSDirectory。github

具體過程以下:數組

  1. 客戶端經過ClientProtocol協議向RpcServer發起建立目錄的RPC請求。
  2. FSNamesystem封裝了各類HDFS操做的實現細節,RpcServer調用FSNamesystem中的相關方法以建立目錄。
  3. 進一步的,FSDirectory封裝了各類目錄樹操做的實現細節,FSNamesystem調用FSDirectory中的相關方法在目錄樹中建立目標目錄,並經過日誌系統備份文件系統的修改。
  4. 最後,RpcServer將RPC響應返回給客戶端。

建立目錄的RPC接口爲ClientProtocol#mkdirs():性能優化

public boolean mkdirs(String src, FsPermission masked, boolean createParent) throws AccessControlException, FileAlreadyExistsException, FileNotFoundException, NSQuotaExceededException, ParentNotDirectoryException, SafeModeException, UnresolvedLinkException, SnapshotAccessControlException, IOException;
複製代碼

對應的RPCServer實現爲NameNodeRpcServer#mkdirs()。bash

後文將以NameNodeRpcServer#mkdirs()爲主流程進行分析。多線程

文章的組織結構

  1. 若是隻涉及單個分支的分析,則放在同一節。
  2. 若是涉及多個分支的分析,則在下一級分多個節,每節討論一個分支。
  3. 多線程的分析同多分支。
  4. 每個分支和線程的組織結構遵循規則1-3。

發起RPC請求

建立一個目錄/test/mkdir,觸發提早設置好的斷點:併發

./bin/hadoop fs -mkdir -p /test/mkdir
複製代碼

執行命令前,該目錄不存在,同時-p選項會遞歸建立不存在的父目錄。根目錄是啓動namenode時建立(或從備份中加載)的,則理想狀況下,HDFS會前後建立/test/mkdir兩個目錄,最後成功返回。app

主流程:NameNodeRpcServer#mkdirs()

NameNodeRpcServer#mkdirs():

public boolean mkdirs(String src, FsPermission masked, boolean createParent) throws IOException {
    if(stateChangeLog.isDebugEnabled()) {
      stateChangeLog.debug("*DIR* NameNode.mkdirs: " + src);
    }
    // 檢查目標目錄的字符串長度(不超過8000)和路徑深度(不超過1000)
    if (!checkPathLength(src)) {
      throw new IOException("mkdirs: Pathname too long. Limit " 
                            + MAX_PATH_LENGTH + " characters, " + MAX_PATH_DEPTH + " levels.");
    }
    // 一般NameNodeRpcServer中的RPC方法實現只有一些簡單的檢查,細節都交給FSNamesystem
    return namesystem.mkdirs(src,
        new PermissionStatus(getRemoteUser().getShortUserName(),
            null, masked), createParent);
  }
複製代碼

一般NameNodeRpcServer只作一些簡單的檢查,而後直接調用FSNamesystem中的同名方法。

FSNamesystem#mkdirs():

boolean mkdirs(String src, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
    boolean ret = false;
    try {
      ret = mkdirsInt(src, permissions, createParent);
    } catch (AccessControlException e) {
      logAuditEvent(false, "mkdirs", src);
      throw e;
    }
    return ret;
  }
  
  ...

  private boolean mkdirsInt(final String srcArg, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
    String src = srcArg;
    if(NameNode.stateChangeLog.isDebugEnabled()) {
      NameNode.stateChangeLog.debug("DIR* NameSystem.mkdirs: " + src);
    }
    // 進一步檢查路徑
    if (!DFSUtil.isValidName(src)) {
      throw new InvalidPathException(src);
    }
    FSPermissionChecker pc = getPermissionChecker();
    // 不加鎖檢查HA狀態下的權限
    checkOperation(OperationCategory.WRITE);
    // 將src中的每層目錄轉成一個byte[],多個層級組成byte[][]
    byte[][] pathComponents = FSDirectory.getPathComponentsForReservedPath(src);
    HdfsFileStatus resultingStat = null;
    boolean status = false;
    writeLock();
    try {
      // 加鎖 re-check HA狀態下的權限
      checkOperation(OperationCategory.WRITE);
      checkNameNodeSafeMode("Cannot create directory " + src);
      src = resolvePath(src, pathComponents);
      // 見下
      status = mkdirsInternal(pc, src, permissions, createParent);
      if (status) {
        resultingStat = getAuditFileInfo(src, false);
      }
    } finally {
      writeUnlock();
    }
    // 手動將日誌同步到磁盤
    getEditLog().logSync();
    if (status) {
      logAuditEvent(true, "mkdirs", srcArg, null, resultingStat);
    }
    return status;
  }
複製代碼

在繼續分析FSNamesystem#mkdirsInternal()以前。須要注意幾個要點,在之後的分析中會無數次重逢:

  • 27行未加鎖獲取的HA狀態多是無效的,該次檢查僅爲了「在不下降併發的狀況下減小資源浪費」,在獲取鎖後須要35行的re-check。
  • namenode上的路徑多以byte[]的形式存在,序列化友好。29行爲了方便後面的比較和處理,將src分層級轉成了byte[][]。
  • 39行建立目錄會修改命名空間,觸發備份機制。而checkpoint備份機制對優化了磁盤寫操做,不會主動將日誌同步到磁盤中。所以,47行須要手動同步日誌。
    • 無論成功失敗都須要同步。這是由於,就算失敗,也可能已經建立了部分父目錄,須要同步。

FSNamesystem#mkdirsInternal():

private boolean mkdirsInternal(FSPermissionChecker pc, String src, PermissionStatus permissions, boolean createParent) throws IOException, UnresolvedLinkException {
    assert hasWriteLock();
    ...// 目錄的權限檢查等
    // 若是createParent爲false,則須要檢查父目錄是否存在。此處createParent爲true,不須要檢查
    if (!createParent) {
      verifyParentDir(src);
    }

    // 檢查inode和block的總數是否超過的namenode限制的對象最大數量${dfs.namenode.max.objects},默認不限制
    checkFsObjectLimit();

    // 不然,遞歸建立全部目錄
    if (!mkdirsRecursively(src, permissions, false, now())) {
      throw new IOException("Failed to create directory: " + src);
    }
    // 不論是否建立成功,都返回true
    return true;
  }
複製代碼

咱們在FS Shell中使用了-p選項,對應createParent參數爲true,則不須要檢查父目錄是否存在,調用FSNamesystem#mkdirsRecursively()遞歸建立全部目錄。隨着後面的分析,咱們會知道,除非存在異常,不然不論是否建立成功,FSNamesystem#mkdirsRecursively()必定返回true。

「遞歸」一詞只是直譯,實際的實現是循環。

此處「mkdirs - mkdirsInt - mkdirsInternal」三級結構,是namenode處理客戶端RPC請求時經常使用的編碼風格,之後分析其餘RPC請求的處理流程時也會看到。

FSNamesystem#mkdirsRecursively()從第一級不存在的目錄開始,逐級建立全部目錄層級:

private boolean mkdirsRecursively(String src, PermissionStatus permissions, boolean inheritPermission, long now) throws FileAlreadyExistsException, QuotaExceededException, UnresolvedLinkException, SnapshotAccessControlException, AclException {
    src = FSDirectory.normalizePath(src);
    // 按層級順序劃分的String[]
    String[] names = INode.getPathNames(src);
    // components是names數組的字節表示形式,參考FSNamesystem#mkdirs()
    byte[][] components = INode.getPathComponents(names);
    final int lastInodeIndex = components.length - 1;

    dir.writeLock();
    try {
      // INodesInPath封裝了按照層級組織components與inodes
      INodesInPath iip = dir.getExistingPathINodes(components);
      ...// 快照相關
      INode[] inodes = iip.getINodes();

      // 將i移動到第一個目錄不存在(inodes[i] == null)的層級
      StringBuilder pathbuilder = new StringBuilder();
      int i = 1;
      for(; i < inodes.length && inodes[i] != null; i++) {
        pathbuilder.append(Path.SEPARATOR).append(names[i]);
        // 目錄的全部祖先目錄必定要是目錄,不然拋出IOE
        if (!inodes[i].isDirectory()) {
          throw new FileAlreadyExistsException(
                  "Parent path is not a directory: "
                  + pathbuilder + " "+inodes[i].getLocalName());
        }
      }
      
      ...// 權限檢查

      // 從第一級不存在的目錄開始,逐級建立全部目錄層級
      for(; i < inodes.length; i++) {
        pathbuilder.append(Path.SEPARATOR).append(names[i]);
        // 在目錄樹中建立目錄節點,見後
        dir.unprotectedMkdir(allocateNewInodeId(), iip, i, components[i],
                (i < lastInodeIndex) ? parentPermissions : permissions, null,
                now);
        // 若是發現inodes[i]爲null表示有異常,返回false通知外層拋出IOE
        if (inodes[i] == null) {
          return false;
        }
        // 小知識點:目錄建立也包含在 FilesCreated 的統計內
        NameNode.getNameNodeMetrics().incrFilesCreated();

        final String cur = pathbuilder.toString();
        // 記錄建立目錄的日誌
        getEditLog().logMkDir(cur, inodes[i]);
        if(NameNode.stateChangeLog.isDebugEnabled()) {
          NameNode.stateChangeLog.debug(
                  "mkdirs: created directory " + cur);
        }
      }
    } finally {
      dir.writeUnlock();
    }
    // 除了異常狀況,統一返回true
    return true;
  }
複製代碼

INodesInPath按照目錄的層級組織目錄名與INode節點,分別保存在INodesInPath#path(byte[][])與INodesInPath#inodes(INode[])中。在建立節點(目錄節點/文件節點,對應建立目錄/建立文件操做)期間,INodesInPath#inodes的後續位置可能爲null,表示該層級的INode還未建立(見FSNamesystem#mkdirsRecursively()方法);在節點建立後,將對應位置置爲目標節點(見FSDirectory#unprotectedMkdir()方法)。

回顧發起RPC請求時的前提條件:當前命名空間中不存在/test目錄。則INodesInPath#path(components)的三級應分別對應""(byte[0])、"test"byte[4]、"mkdir"byte[5],INodesInPath#inodes的三級分別對應INodeDirectory "/"nullnull。所以,這裏要建立的第一級目錄是/test,建立該目錄時知足i == 1;而後依次建立/mkdir目錄,知足i == 2。看IDE的變量提示驗證:

image.png

PS:此處偷懶用了後面FSDirectory#unprotectedMkdir()方法的inodesInPath參數的配圖。不過截圖時尚未修改inodesInPath,與此處iip的引用是相等的,沒有影響。

INodesInPath的結構在目錄樹操做中很是經常使用。

正常狀況下,39行建立目錄節點後,目錄節點必定已經建立,inodes[i]必定不爲null。所以,若是43行發現inodes[i]仍爲null,就說明39行執行過程當中發生了異常狀況,返回false通知外層拋出IOE(回顧FSNamesystem#mkdirsInternal()方法)。

另外,39行FSDirectory#unprotectedMkdir()的「不受保護」指「不包含備份機制的相關邏輯」。所以,每建立一級目錄後(修改目錄樹),都要執行52行FSNamesystem#getEditLog#logMkDir()在日誌中記錄建立的目錄和inode。"FSDirectory#unprotectedMkdir() + FSNamesystem#getEditLog#logMkDir()"組合是namenode備份機制的經常使用編碼風格,之後的分析中也很常見。

注意39行、43行、52行三處邏輯的配合:若是39行目錄建立異常,將提早經過43行return false,不會觸發52行的備份。

最後,若是不考慮43行的影響,則無論FSDirectory#unprotectedMkdir()是否執行成功,FSNamesystem#mkdirsRecursively()都會返回true。從這個角度上看,mkdir是一個冪等操做;繼續後面的分析可進一步得知,當且僅當目錄已存在時,FSDirectory#unprotectedMkdir()會返回false,此時偏偏不須要建立目錄,保證了冪等性。

FSDirectory#unprotectedMkdir():

void unprotectedMkdir(long inodeId, INodesInPath inodesInPath, int pos, byte[] name, PermissionStatus permission, List<AclEntry> aclEntries, long timestamp) throws QuotaExceededException, AclException {
    assert hasWriteLock();
    // 建立目標的目錄節點 dir。此處即"test"目錄
    final INodeDirectory dir = new INodeDirectory(inodeId, name, permission,
        timestamp);
    // 將目錄節點 dir 添加到目錄樹中,見後
    if (addChild(inodesInPath, pos, dir, true)) {
      if (aclEntries != null) {
        AclStorage.updateINodeAcl(dir, aclEntries, Snapshot.CURRENT_STATE_ID);
      }
      // 將第pos級(從0開始)INode 設置爲 dir(以前是null),見後
      inodesInPath.setINode(pos, dir);
    }
  }
複製代碼

仍以建立"test"目錄爲例。根據FSNamesystem#mkdirsRecursively()方法的分析,此處傳入的pos爲1,name爲"test"的byte[]形式,inodesInPath.inodes[0]指向「test」目錄的父目錄節點,inodesInPath.inodes[1]爲null。

在10行成功添加節點後,15行將第pos級(從0開始)INode設置爲dir以前是null)。也就是FSNamesystem#mkdirsRecursively()中介紹的InodesInPath用法。

如今看19行的FSDirectory#addChild():

private boolean addChild(INodesInPath iip, int pos, INode child, boolean checkQuota) throws QuotaExceededException {
    final INode[] inodes = iip.getINodes();
    // 檢查路徑是否與保留路徑"/.reserved"衝突
    if (pos == 1 && inodes[0] == rootDir && isReservedName(child)) {
      throw new HadoopIllegalArgumentException(
          "File name \"" + child.getLocalName() + "\" is reserved and cannot "
              + "be created. If this is during upgrade change the name of the "
              + "existing file or directory to another name before upgrading "
              + "to the new release.");
    }
    // 此處傳入的checkQuota爲true,須要檢查quota
    if (checkQuota) {
      // 限制節點名最大長度${dfs.namenode.fs-limits.max-component-length},默認255
      verifyMaxComponentLength(child.getLocalNameBytes(), inodes, pos);
      // 限制子節點最大數量${dfs.namenode.fs-limits.max-directory-items},默認1024*1024
      verifyMaxDirItems(inodes, pos);
    }
    
    // 檢查節點名是否與保留名".snapshot"衝突
    verifyINodeName(child.getLocalNameBytes());
    
    // 更新quota
    final Quota.Counts counts = child.computeQuotaUsage();
    updateCount(iip, pos,
        counts.get(Quota.NAMESPACE), counts.get(Quota.DISKSPACE), checkQuota);
    
    // 判斷是不是rename操做。暫時忽略
    boolean isRename = (child.getParent() != null);
    // 根據對INodesInPath的分析,第pos - 1級節點即當前節點的父目錄
    final INodeDirectory parent = inodes[pos-1].asDirectory();
    boolean added;
    try {
      // 將child添加到父目錄節點parent下,返回是否添加成功的標誌(存在同名節點返回false,不然返回true),見後
      added = parent.addChild(child, true, iip.getLatestSnapshotId());
    } catch (QuotaExceededException e) {
      // 發生異常,則以前的更新是多餘的,回退quota
      updateCountNoQuotaCheck(iip, pos,
          -counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
      throw e;
    }
    if (!added) {   // 若是節點已存在,則以前的更新是多餘的,回退quota
      updateCountNoQuotaCheck(iip, pos,
          -counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
    } else {    // 不然,表示已經成功添加的節點
      // 沒明白這裏設置父目錄幹嗎??
      iip.setINode(pos - 1, child.getParent());
      if (!isRename) {
        AclStorage.copyINodeDefaultAcl(child);
      }
      // 將child節點添加至FSDirectory#inodeMap
      addToInodeMap(child);
    }
    return added;
  }
複製代碼

傳入的pos爲1,child爲外層建立的dir,checkQuota爲true。暫時不考慮rename操做,那麼思路很簡單:

  1. 最後的檢查
  2. 將child添加到父目錄節點parent下
  3. 將child節點添加至FSDirectory#inodeMap

補充下如何判斷rename操做:

29行,若是child的父目錄不爲null(FSDirectory#unprotectedMkdir()中建立該節點時父目錄爲null),則認爲是rename操做。若是是添加節點的操做,則35行INodeDirectory#addChild()添加節點成功後會將child的父目錄置爲parent,所以,再次進入FSDirectory#addChild()時必然是rename操做。

INodeDirectory#addChild():

public boolean addChild(INode node, final boolean setModTime, final int latestSnapshotId) throws QuotaExceededException {
    // 在孩子節點列表中查找同名節點
    final int low = searchChildren(node.getLocalNameBytes());
    // 若是目標節點已存在,則返回false
    if (low >= 0) {
      return false;
    }

    ...// 快照相關
    addChild(node, low);
    if (setModTime) {
      updateModificationTime(node.getModificationTime(), latestSnapshotId);
    }
    // 不然,添加目標節點後返回true
    return true;
  }
  
  ...
  
  private void addChild(final INode node, final int insertionPoint) {
    if (children == null) {
      children = new ArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
    }
    // 設置節點node的父目錄
    node.setParent(this);
    // 在孩子節點中檢查是否存在同名節點
    children.add(-insertionPoint - 1, node);

    if (node.getGroupName() == null) {
      node.setGroup(getGroupName());
    }
  }
  
  ...
  
  int searchChildren(byte[] name) {
    return children == null? -1: Collections.binarySearch(children, name);
  }
複製代碼

當前節點即parent,傳入的node即外層的child。重載的兩個INodeDirectory#addChild()方法很簡單,注意這裏的一個性能優化點便可:

  • INodeDirectory#children維護了孩子節點的有序列表。
  • 4行INodeDirectory#searchChildren()的內部實現是一個二分查找:若是不存在同名節點,則返回 -insertionIndex - 1,表示在insertionIndex位置插入目標節點後不破壞有序性;不然,返回節點序號low >= 0,說明存在同名節點,插入失敗,6-8行返回false。此處不存在任何孩子節點,所以insertionIndex = 0,則low = -1,繼續執行。
  • 28行insertionPoint即外層的low,知足insertionPoint == -insertionIndex - 1,則插入位置知足insertionIndex == -insertionPoint - 1。如上,在insertionIndex位置插入目標節點,不會破壞INodeDirectory#children的有序性。

24行設置父目錄,也就是FSDirectory#addChild()中介紹的rename操做判斷方法。

至此,第一級目錄"test"的建立已經分析完畢。接下來,程序將回到FSNamesystem#mkdirsRecursively()繼續建立第二級目錄"mkdir"。最後,將建立結果返回給客戶端。

總結

namenode建立目錄的過程只涉及文件元信息的操做,邏輯相對簡單。歸納起來,需掌握幾個點:

  • 各關鍵組件的交互方式:
    • 最外層:NameNodeRpcServer#mkdirs()簡單檢查,主要邏輯交給FSNamesystem#mkdirs()。
    • 中間層:FSNamesystem中的「mkdirs - mkdirsInt - mkdirsInternal」三級結構。
    • 最內層:FSNamesystem最終封裝"FSDirectory#unprotectedMkdir() + FSEditLog#logMkDir()"組合。
  • INodesInPath的結構與用法,特別是INodesInPath#inodes的用法

本文連接:源碼|HDFS之NameNode:建立目錄
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索