原始碼|HDFS之NameNode:啟動過程

monkeysayhi發表於2019-02-23

仿照原始碼|HDFS之DataNode:啟動過程,NameNode也從啟動過程開始。

namenode的啟動過程與HA緊緊繫結在一起,但本文暫不討論HA相關內容,以後再填HA的坑。

原始碼版本:Apache Hadoop 2.6.0

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

開始之前

總覽

namenode的啟動過程圍繞著safemode、HA等展開,啟動之後,各種工作執行緒開始發揮作用。主要包括:

  • 載入fsimage與editlog
  • 啟動多種工作執行緒,主要包括:
    • 通訊:RpcServer
    • 監控:JVMPauseMonitor、PendingReplicationMonitor、DecommissionManager#Monitor、HeartbeatManager#Monitor、ReplicationMonitor、LeaseManager#Monitor、NameNodeResourceMonitor
    • 其他:HttpServer (Web UI)、NameNodeEditLogRoller
  • HA相關(暫不討論)
  • 關閉safemode

CacheManager#CacheReplicationMonitor等暫不討論。

檔案管理機制

namenode通過FSNamesystem管理檔案後設資料。具體來講:

  • 通過FSDirectory管理檔案系統的名稱空間
  • 通過BlockManager管理檔案到資料塊的對映和資料塊到資料節點的對映

namenode上的資料塊狀態

與yarn不同,hdfs並沒有直接用狀態機來管理block,而是將不同狀態的block儲存在不同的緩衝區中,狀態遷移則對應資料塊在不同緩衝區中的移動。包括BlockManager#blocksMap,namenode上的資料塊狀態共涉及以下幾種緩衝區:

  • 正在寫入的資料塊:通過LeaseManager掃描INodeFile,BlockInfo#isComplete()返回false即為正在寫入的資料塊(不常用)
  • 儲存資料塊的元資訊,可以認為儲存已完成等所有狀態的資料塊:BlockManager#blocksMap,BlockInfo#isComplete()返回true
  • 需要複製的資料塊:BlockManager#neededReplications
  • 正在複製的資料塊:BlockManager#pendingReplications
  • 複製超時的資料塊:BlockManager#pendingReplications#timedOutItems
  • 多餘的資料塊:BlockManager#excessReplicateMap(即需要刪除的資料塊)
  • 無效資料塊緩衝區BlockManager#invalidateBlocks(即正在刪除的資料塊)

BlockManager#excessReplicateMap與BlockManager#neededReplications對應,BlockManager#invalidateBlocks與BlockManager#pendingReplications對應,為了統一,本文相應稱BlockManager#excessReplicateMap為“需要刪除資料塊緩衝區”,稱BlockManager#invalidateBlocks為“正在刪除資料塊緩衝區”。

其中,與資料塊(對應多個副本)寫入密切相關的LeaseManager;與副本複製任務密切相關的是BlockManager#neededReplications、BlockManager#pendingReplications、BlockManager#pendingReplications#timedOutItems,與副本刪除密切相關的是BlockManager#excessReplicateMap、BlockManager#invalidateBlocks。

詳細的狀態轉換邏輯在分析完原始碼後,再來總結。

文章的組織結構

  1. 如果只涉及單個分支的分析,則放在同一節。
  2. 如果涉及多個分支的分析,則在下一級分多個節,每節討論一個分支。
  3. 多執行緒的分析同多分支。
  4. 每一個分支和執行緒的組織結構遵循規則1-3。

主流程

namenode的Main Class是NameNode,先找NameNode.main():

  public static void main(String argv[]) throws Exception {
    if (DFSUtil.parseHelpArgument(argv, NameNode.USAGE, System.out, true)) {
      System.exit(0);
    }

    try {
      StringUtils.startupShutdownMessage(NameNode.class, argv, LOG);
      // 建立namenode
      NameNode namenode = createNameNode(argv, null);
      // 等待namenode關閉
      if (namenode != null) {
        namenode.join();
      }
    } catch (Throwable e) {
      LOG.fatal("Failed to start namenode.", e);
      terminate(1, e);
    }
  }
  
  ...
  
  public void join() {
    try {
      // 等待RPCServer關閉,其他守護程式會自動關閉
      rpcServer.join();
    } catch (InterruptedException ie) {
      LOG.info("Caught interrupted exception ", ie);
    }
  }
複製程式碼

NameNode#join()等待namenode關閉,基本邏輯同datanode。

主要看NameNode.createNameNode():

  public static NameNode createNameNode(String argv[], Configuration conf)
      throws IOException {
    LOG.info("createNameNode " + Arrays.asList(argv));
    if (conf == null)
      conf = new HdfsConfiguration();
    // 解析啟動選項
    StartupOption startOpt = parseArguments(argv);
    if (startOpt == null) {
      printUsage(System.err);
      return null;
    }
    setStartupOption(conf, startOpt);

    switch (startOpt) {
      ...// 其他分支
      default: {
        // 正常啟動的話
        // 初始化metric系統
        DefaultMetricsSystem.initialize("NameNode");
        // 建立NameNode
        return new NameNode(conf);
      }
    }
  }
複製程式碼

正常啟動的話,滿足startOpt == StartupOption.REGULAR,會走到default分支。

NameNode.<init>()

  public NameNode(Configuration conf) throws IOException {
    this(conf, NamenodeRole.NAMENODE);
  }
  
  ...

  protected NameNode(Configuration conf, NamenodeRole role) 
      throws IOException { 
    this.conf = conf;
    this.role = role;
    // 設定NameNode#clientNamenodeAddress為"hdfs://localhost:9000"
    setClientNamenodeAddress(conf);
    String nsId = getNameServiceId(conf);
    String namenodeId = HAUtil.getNameNodeId(conf, nsId);
    // HA相關
    this.haEnabled = HAUtil.isHAEnabled(conf, nsId);
    state = createHAState(getStartupOption(conf));
    this.allowStaleStandbyReads = HAUtil.shouldAllowStandbyReads(conf);
    this.haContext = createHAContext();
    try {
      initializeGenericKeys(conf, nsId, namenodeId);
      // 完成實際的初始化工作
      initialize(conf);
      // HA相關
      try {
        haContext.writeLock();
        state.prepareToEnterState(haContext);
        state.enterState(haContext);
      } finally {
        haContext.writeUnlock();
      }
    } catch (IOException e) {
      this.stop();
      throw e;
    } catch (HadoopIllegalArgumentException e) {
      this.stop();
      throw e;
    }
  }
複製程式碼

這裡要特別說明部分HA的內容:

儘管本地的偽分散式叢集無法開啟HA(對應NameNode#haEnabled為false),namenode仍然擁有一個HAState,此時,namenode會被標記為active(對應HAState ACTIVE_STATE = new ActiveState()),然後在ActiveState#enterState()中啟動LeaseManager#Monitor、NameNodeEditLogRoller等。

具體來講,在NameNode#initialize()完成實際的初始化工作返回後,還要執行ActiveState#enterState(),完成一些只有active狀態namenode才應該做的工作,如:

  • 開啟FsDirectory的quota檢查
  • 啟動LeaseManager#Monitor
  • 啟動NameNodeResourceMonitor
  • 啟動NameNodeEditLogRoller
  • 啟動CacheManager#CacheReplicationMonitor等

後面會專門討論HA機制,讀者知道何時啟動了這些工作執行緒即可。

下面繼續看NameNode#initialize():

  protected void initialize(Configuration conf) throws IOException {
    if (conf.get(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS) == null) {
      String intervals = conf.get(DFS_METRICS_PERCENTILES_INTERVALS_KEY);
      if (intervals != null) {
        conf.set(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS,
          intervals);
      }
    }

    UserGroupInformation.setConfiguration(conf);
    loginAsNameNodeUser(conf);

    // 初始化metric
    NameNode.initMetrics(conf, this.getRole());
    StartupProgressMetrics.register(startupProgress);

    // 啟動httpServer
    if (NamenodeRole.NAMENODE == role) {
      startHttpServer(conf);
    }

    this.spanReceiverHost = SpanReceiverHost.getInstance(conf);

    // 從`${dfs.namenode.name.dir}`目錄載入fsimage與editlog,初始化FsNamesystem、FsDirectory、LeaseManager等
    loadNamesystem(conf);

    // 建立RpcServer,封裝了NameNodeRpcServer#clientRpcServer,支援ClientNamenodeProtocol、DatanodeProtocolPB等協議
    rpcServer = createRpcServer(conf);
    if (clientNamenodeAddress == null) {
      // This is expected for MiniDFSCluster. Set it now using 
      // the RPC server`s bind address.
      clientNamenodeAddress = 
          NetUtils.getHostPortString(rpcServer.getRpcAddress());
      LOG.info("Clients are to use " + clientNamenodeAddress + " to access"
          + " this namenode/service.");
    }
    if (NamenodeRole.NAMENODE == role) {
      httpServer.setNameNodeAddress(getNameNodeAddress());
      httpServer.setFSImage(getFSImage());
    }
    
    // 啟動JvmPauseMonitor等,反向監控JVM
    pauseMonitor = new JvmPauseMonitor(conf);
    pauseMonitor.start();
    metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
    
    // 啟動多項重要的工作執行緒
    startCommonServices(conf);
  }
複製程式碼

實際上,NameNode#loadNamesystem()非常重要,但限於篇幅和精力,猴子只是大概追蹤了下流程,很多細節來不及分析,就不在此處展開了。

當前namenode的角色為NamenodeRole.NAMENODE,則此處啟動HttpServer;JvmPauseMonitor也在此處啟動。

重頭戲是NameNode#startCommonServices():

  private void startCommonServices(Configuration conf) throws IOException {
    // 建立NameNodeResourceChecker、啟用BlockManager等
    namesystem.startCommonServices(conf, haContext);
    registerNNSMXBean();
    // 角色非`NamenodeRole.NAMENODE`的在此處啟動HttpServer
    if (NamenodeRole.NAMENODE != role) {
      startHttpServer(conf);
      httpServer.setNameNodeAddress(getNameNodeAddress());
      httpServer.setFSImage(getFSImage());
    }
    // 啟動RPCServer
    rpcServer.start();
    ...// 啟動各外掛
    LOG.info(getRole() + " RPC up at: " + rpcServer.getRpcAddress());
    if (rpcServer.getServiceRpcAddress() != null) {
      LOG.info(getRole() + " service RPC up at: "
          + rpcServer.getServiceRpcAddress());
    }
  }
複製程式碼

RPCServer的啟動很簡單,重點是FSNamesystem#startCommonServices():

  void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
    this.registerMBean(); // register the MBean for the FSNamesystemState
    writeLock();
    this.haContext = haContext;
    try {
      // 建立NameNodeResourceChecker,並立即檢查一次
      nnResourceChecker = new NameNodeResourceChecker(conf);
      checkAvailableResources();
      assert safeMode != null && !isPopulatingReplQueues();
      // 設定一些啟動過程中的資訊
      StartupProgress prog = NameNode.getStartupProgress();
      prog.beginPhase(Phase.SAFEMODE);
      prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
        getCompleteBlocksTotal());
      // 設定已完成的資料塊總量
      setBlockTotal();
      // 啟用BlockManager
      blockManager.activate(conf);
    } finally {
      writeUnlock();
    }
    
    registerMXBean();
    DefaultMetricsSystem.instance().register(this);
    snapshotManager.registerMXBean();
  }
複製程式碼

提醒一下,此時的工作是任何角色的namenode(對於HA來說,即active與standby)都需要做的,“common”即此意。

NameNodeResourceChecker負責檢查磁碟資源。active狀態的namenod會啟動一個監控執行緒NameNodeResourceMonitor,定期執行NameNodeResourceChecker#hasAvailableDiskSpace()檢查可用的磁碟資源。

FSNamesystem#setBlockTotal()設定已完成的資料塊總量completeBlocksTotal = blocksTotal - numUCBlocks。其中,blocksTotal來自BlockManager,numUCBlocks來自LeaseManager,均從備份中恢復而來。

注意,儘管此時BlockManager#blocksMap從備份中“恢復”了全部資料塊,但這些資料塊的BlockInfo#triplets是空的,因為datanode還沒有將資料塊的資訊彙報到namenode(猴子只啟動了namenode)。

BlockManager#activate()所謂的啟用BlockManager,主要完成了PendingReplicationMonitor、DecommissionManager#Monitor、HeartbeatManager#Monitor、ReplicationMonitor等工作執行緒的啟動:

  public void activate(Configuration conf) {
    // 啟動PendingReplicationMonitor
    pendingReplications.start();
    // 啟用DatanodeManager:啟動DecommissionManager#Monitor、HeartbeatManager#Monitor
    datanodeManager.activate(conf);
    // 啟動BlockManager#ReplicationMonitor
    this.replicationThread.start();
  }
複製程式碼

啟動PendingReplicationMonitor:PendingReplicationBlocks#start()

PendingReplicationBlocks#start()啟動PendingReplicationMonitor:

  void start() {
    timerThread = new Daemon(new PendingReplicationMonitor());
    timerThread.start();
  }
複製程式碼

通過PendingReplicationBlocks#timerThread持有實際的PendingReplicationMonitor執行緒。

PendingReplicationMonitor執行緒後文單獨用一個分支分析。

啟用DatanodeManager:DatanodeManager#activate()

DatanodeManager#activate()啟動DecommissionManager#Monitor、HeartbeatManager#Monitor:

  void activate(final Configuration conf) {
    final DecommissionManager dm = new DecommissionManager(namesystem, blockManager);
    this.decommissionthread = new Daemon(dm.new Monitor(
        conf.getInt(DFSConfigKeys.DFS_NAMENODE_DECOMMISSION_INTERVAL_KEY, 
                    DFSConfigKeys.DFS_NAMENODE_DECOMMISSION_INTERVAL_DEFAULT),
        conf.getInt(DFSConfigKeys.DFS_NAMENODE_DECOMMISSION_NODES_PER_INTERVAL_KEY, 
                    DFSConfigKeys.DFS_NAMENODE_DECOMMISSION_NODES_PER_INTERVAL_DEFAULT)));
    // 啟動DecommissionManager#Monitor
    decommissionthread.start();

    // 啟用HeartbeatManager
    heartbeatManager.activate(conf);
  }
複製程式碼

Decommission指datanode的下線操作,暫不關注。主要看HeartbeatManager相關。

HeartbeatManager#activate():

  void activate(Configuration conf) {
    heartbeatThread.start();
  }
複製程式碼

HeartbeatManager#heartbeatThread是一個HeartbeatManager#Monitor執行緒。

HeartbeatManager#Monitor執行緒後文單獨用一個分支分析。

啟動ReplicationMonitor

BlockManager#replicationThread是一個BlockManager#ReplicationMonitor執行緒。

BlockManager#ReplicationMonitor執行緒後文單獨用一個分支分析。

下面分別分析PendingReplicationMonitor執行緒、HeartbeatManager#Monitor執行緒、BlockManager#ReplicationMonitor執行緒,三者與namenode上資料塊的狀態管理密切相關。理解了這三個執行緒的互動關係,有助於以後理解其他資料塊狀態轉換邏輯。

PendingReplicationMonitor執行緒

PendingReplicationMonitor#run():

    public void run() {
      while (fsRunning) {
        long period = Math.min(DEFAULT_RECHECK_INTERVAL, timeout);
        try {
          // 檢查正在複製的資料塊是否超時
          pendingReplicationCheck();
          Thread.sleep(period);
        } catch (InterruptedException ie) {
          if(LOG.isDebugEnabled()) {
            LOG.debug("PendingReplicationMonitor thread is interrupted.", ie);
          }
        }
      }
    }
複製程式碼

定期執行PendingReplicationMonitor#pendingReplicationCheck()檢查正在複製的資料塊是否超時:

    void pendingReplicationCheck() {
      synchronized (pendingReplications) {
        Iterator<Map.Entry<Block, PendingBlockInfo>> iter =
                                    pendingReplications.entrySet().iterator();
        long now = now();
        if(LOG.isDebugEnabled()) {
          LOG.debug("PendingReplicationMonitor checking Q");
        }
        // 遍歷`正在複製資料塊緩衝區`
        while (iter.hasNext()) {
          Map.Entry<Block, PendingBlockInfo> entry = iter.next();
          PendingBlockInfo pendingBlock = entry.getValue();
          // 如果資料塊超時,則將其新增到`複製超時資料塊緩衝區`
          if (now > pendingBlock.getTimeStamp() + timeout) {
            Block block = entry.getKey();
            synchronized (timedOutItems) {
              timedOutItems.add(block);
            }
            LOG.warn("PendingReplicationMonitor timed out " + block);
            iter.remove();
          }
        }
      }
    }
複製程式碼

遍歷正在複製資料塊緩衝區BlockManager#pendingReplications如果發現有資料塊超時,則將資料塊新增到複製超時資料塊緩衝區BlockManager#pendingReplications#timedOutItems

超時指超過${dfs.namenode.replication.pending.timeout-sec}毫秒(儘管字尾是“sec”,但程式碼邏輯是毫秒)仍沒有收到副本複製成功的響應。

HeartbeatManager#Monitor執行緒

HeartbeatManager#Monitor#run():

    public void run() {
      while(namesystem.isRunning()) {
        try {
          final long now = Time.now();
          if (lastHeartbeatCheck + heartbeatRecheckInterval < now) {
            // 根據心跳資訊檢查節點與資料目錄
            heartbeatCheck();
            lastHeartbeatCheck = now;
          }
          ...// 安全認證相關,暫時忽略
        } catch (Exception e) {
          LOG.error("Exception while checking heartbeat", e);
        }
        try {
          Thread.sleep(5000);  // 5 seconds
        } catch (InterruptedException ie) {
        }
      }
    }
複製程式碼

定期執行HeartbeatManager#heartbeatCheck()根據心跳資訊檢查節點與資料目錄:

  void heartbeatCheck() {
    final DatanodeManager dm = blockManager.getDatanodeManager();
    // 如果處於啟動過程中的safemode狀態,則不進行檢查(空檢查)
    if (namesystem.isInStartupSafeMode()) {
      return;
    }
    // 下述邏輯保證每次只移除一個datanode或資料目錄
    boolean allAlive = false;
    while (!allAlive) {
      // 記錄第一個死亡的datanode
      DatanodeID dead = null;
      // 記錄非死亡datanode上的第一個失敗資料目錄
      DatanodeStorageInfo failedStorage = null;

      // 檢查節點與資料目錄
      int numOfStaleNodes = 0;
      int numOfStaleStorages = 0;
      synchronized(this) {
        for (DatanodeDescriptor d : datanodes) {
          // 檢查節點是否死亡或不新鮮
          if (dead == null && dm.isDatanodeDead(d)) {
            stats.incrExpiredHeartbeats();
            dead = d;
          }
          if (d.isStale(dm.getStaleInterval())) {
            numOfStaleNodes++;
          }
          // 如果節點儲存且新鮮,則檢查節點上的所有資料目錄是否不新鮮或失敗
          DatanodeStorageInfo[] storageInfos = d.getStorageInfos();
          for(DatanodeStorageInfo storageInfo : storageInfos) {
            if (storageInfo.areBlockContentsStale()) {
              numOfStaleStorages++;
            }

            if (failedStorage == null &&
                storageInfo.areBlocksOnFailedStorage() &&
                d != dead) {
              failedStorage = storageInfo;
            }
          }

        }
        
        dm.setNumStaleNodes(numOfStaleNodes);
        dm.setNumStaleStorages(numOfStaleStorages);
      }

      // 更新allAlive,如果全部通過了檢查,則退出迴圈
      allAlive = dead == null && failedStorage == null;
      if (dead != null) {
        ...// 移除第一個死亡資料節點及與其關聯的副本
      }
      if (failedStorage != null) {
        ...// 移除第一個失敗資料目錄關聯的副本
      }
    }
  }
複製程式碼

在datanode、資料目錄兩個粒度進行檢查,以確定其是否死亡或不新鮮。如果datanode死亡,則移除該datanode,且不再需要刪除該datanode上的副本,從需要刪除資料塊緩衝區BlockManager#excessReplicateMap正在刪除資料塊緩衝區BlockManager#invalidateBlocks中移除相關副本。如果有資料目錄失敗,則只需要移除與其關聯的副本。

  • 不新鮮是介於存活與死亡之間的一個狀態。不新鮮與死亡狀態都通過心跳間隔判斷,閾值不同。
  • 猴子有時候說“副本”,有時候說“資料塊”,可能會讓讀者感到糊塗。與資料塊相比,副本與具體的某個datanode繫結。可以理解為,資料塊一個抽象概念,對應多個副本,每個副本與一個datanode繫結

BlockManager#ReplicationMonitor執行緒

BlockManager#ReplicationMonitor#run():

    public void run() {
      while (namesystem.isRunning()) {
        try {
          // Process replication work only when active NN is out of safe mode.
          if (namesystem.isPopulatingReplQueues()) {
            // 計算副本複製與副本刪除任務
            computeDatanodeWork();
            // 處理過期的副本複製任務
            processPendingReplications();
          }
          Thread.sleep(replicationRecheckInterval);
        } catch (Throwable t) {
          ...// 異常處理
        }
      }
    }
複製程式碼

如果開啟了HA,則_當且僅當當前namenode處於active狀態,且safemode關閉的狀態下,FsNamesystem#isPopulatingReplQueues()才會返回true_;否則返回false。

根據前文對NameNode.<init>()方法的分析,如果未開啟HA,namenode將被標記為active狀態;啟動過程中,將開啟safemode。那麼,何時才會關閉safemode呢?等到彙報的資料塊的比例超過設定的閾值,就會關閉safemode,標誌著namenode啟動過程的完成。

現在啟動一個datenode(猴子之前向叢集上傳過幾個檔案),等待第一次資料塊彙報完成後,繼續來分析BlockManager#computeDatanodeWork()與BlockManager#processPendingReplications()。

BlockManager#computeDatanodeWork()

BlockManager#computeDatanodeWork():

  int computeDatanodeWork() {
    if (namesystem.isInSafeMode()) {
      return 0;
    }

    // 流控相關
    final int numlive = heartbeatManager.getLiveDatanodeCount();
    final int blocksToProcess = numlive
        * this.blocksReplWorkMultiplier;
    final int nodesToProcess = (int) Math.ceil(numlive
        * this.blocksInvalidateWorkPct);

    // 計算可進行副本複製的任務,最後返回任務數
    int workFound = this.computeReplicationWork(blocksToProcess);

    // 更新狀態等
    namesystem.writeLock();
    try {
      this.updateState();
      this.scheduledReplicationBlocksCount = workFound;
    } finally {
      namesystem.writeUnlock();
    }
    // 計算可進行副本刪除的任務,最後返回任務數
    workFound += this.computeInvalidateWork(nodesToProcess);
    // 返回總任務數
    return workFound;
  }
複製程式碼

BlockManager#computeReplicationWork()遍歷需要複製資料塊緩衝區BlockManager#neededReplications,計算可進行副本複製的任務,放入正在複製資料塊緩衝區BlockManager#pendingReplications,最後返回任務數。

BlockManager#computeInvalidateWork()遍歷需要刪除資料塊緩衝區BlockManager#excessReplicateMap,計算可進行副本刪除的任務,放入正在刪除資料塊緩衝區BlockManager#invalidateBlocks,最後返回任務數。

namenode對副本複製和副本刪除做了一些簡單的“流控”(將工作理解為網路流量,對工作數的控制):

  • 對副本複製:限制每批次進行副本複製的資料塊總數,最多為叢集存活datanode總數的${dfs.namenode.replication.work.multiplier.per.iteration}倍,預設為2。
  • 對副本刪除:限制每批次進行副本刪除涉及的datanode總數,最多為叢集存活datanode總數的${dfs.namenode.invalidate.work.pct.per.iteration}倍,預設為32%;限制每批次涉及的每個datanode上刪除的副本總數,最多為${dfs.block.invalidate.limit},預設為1000。

例如叢集有1000個存活節點,使用預設引數,則每批次最多建立1000 * 2 = 2000個資料塊的副本複製工作,最多建立1000 * 32% * 1000 = 32w個副本。

可以看到,副本複製的任務數上限遠大於副本刪除。這是因為,副本複製需要在datanode之間複製資料塊,佔用大量網路資源,如果不限制同時進行的副本複製任務數,很容易造成網路擁塞,影響整個叢集的效能;而副本刪除只涉及datanode內部的操作,甚至,對於大部分作業系統而言,檔案remove操作只需要操作類似檔案分配表(File Allocation table,FAT)的結構,成本非常小。

BlockManager#processPendingReplications()

BlockManager#processPendingReplications():

  private void processPendingReplications() {
    // 獲取全部的複製超時資料塊,並清空複製超時資料塊緩衝區
    Block[] timedOutItems = pendingReplications.getTimedOutBlocks();
    if (timedOutItems != null) {
      namesystem.writeLock();
      try {
        for (int i = 0; i < timedOutItems.length; i++) {
          // 計算各狀態的副本數
          NumberReplicas num = countNodes(timedOutItems[i]);
          // 如果過期的待複製資料塊仍然需要被複制,則將其新增回需要複製資料塊緩衝區
          if (isNeededReplication(timedOutItems[i], getReplication(timedOutItems[i]),
                                 num.liveReplicas())) {
            neededReplications.add(timedOutItems[i],
                                   num.liveReplicas(),
                                   num.decommissionedReplicas(),
                                   getReplication(timedOutItems[i]));
          }
        }
      } finally {
        namesystem.writeUnlock();
      }
    }
  }
  
  ...
  
  private boolean isNeededReplication(Block b, int expected, int current) {
    // 如果當前的存活副本數小於副本系數,或資料塊沒有足夠的機架分佈,就需要繼續複製
    return current < expected || !blockHasEnoughRacks(b);
  }
複製程式碼

BlockManager#processPendingReplications()每次都_從複製超時資料塊緩衝區BlockManager#pendingReplications#timedOutItems中取出全部的複製超時資料塊,如果這些資料塊還需要被複制,則將其重新加入需要複製資料塊緩衝區BlockManager#neededReplications_。等待BlockManager#ReplicationMonitor執行緒在下一批次計算這些副本複製任務。

至此,namenode啟動流程的原始碼已經走完。

總結

datanode的主要責任是資料塊(檔案內容)的讀寫,因此,datanode的啟動流程主要關注的是與客戶端、namenode通訊的工作執行緒,底層的儲存管理機制等。

namenode的主要責任是檔案元資訊與資料塊對映的管理。相應的,namenode的啟動流程需要關注與客戶端、datanode通訊的工作執行緒,檔案元資訊的管理機制,資料塊的管理機制等。其中,RpcServer主要負責與客戶端、datanode通訊,FSDirectory主要負責管理檔案元資訊,二者中的難點分別為RPC機制和名稱空間備份機制,本文簡單提及,沒有深入。重點筆墨放在了namenode對資料塊的管理上,即namenode上資料塊的關鍵狀態轉換

image.png

上圖使用dot語言 + graphiz + sublime text 3 + graphiz-prefiew製作,簡潔靈活逼格高,從此愛上狀態機。

解釋一下上圖:

  • 開始狀態為complete(因為我們還沒有分析namenode上的寫資料塊流程),結束狀態為completenone
  • none表示刪除後的狀態。
  • none狀態外,各狀態對應著總覽中的各緩衝區。

紅線組成了副本複製的關鍵流程;藍線組成了副本刪除的關鍵流程。與副本複製流程相比,副本刪除流程不需要區分timeout狀態的資料塊(類似複製超時資料塊緩衝區BlockManager#pendingReplications#timedOutItems),更不需要區分刪除失敗等狀態。這是因為,副本刪除命令被髮出後,namenode即認為副本刪除成功如果實際上刪除失敗(超時等原因),datanode必然會再次彙報目標資料塊,namenode發現已經有足夠的存活副本,則將目標資料塊再次加入需要刪除資料塊緩衝區BlockManager#excessReplicateMap,即資料塊再次轉為excess_replicate狀態

此處並不是完整的NNBlock狀態機(仿照Yarn中RMApp的命名),隨著以後的分析,還要引入“資料塊正在寫”、“資料塊損壞”等狀態。

吐槽

最近狀態不好,近一週看不下去原始碼。看原始碼太缺少成就感,時間長了實在熬人,,,有沒有建議?


本文連結:原始碼|HDFS之NameNode:啟動過程
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章