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

monkeysayhi發表於2018-01-29

掌握Mac編譯Hadoop原始碼Hadoop單步debug追原始碼後,就能告別人肉呼叫棧,利用IDE輕鬆愉快的追各種開源框架的原始碼啦~

今天是HDFS中DataNode的第一篇——DataNode啟動過程。

原始碼版本:Apache Hadoop 2.6.0

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

在開始之前

總覽

HDFS-2.x與1.x的核心區別:

  • 為支援Federation,會為每個namespace(或稱nameservice)建立BPOfferService(提供BlockPool服務)
  • 為支援HA,BPOfferService還會為一個namespace下的每個namenode建立BPServiceActor(作為具體例項與各active、standby的namenode通訊;一個BPOfferService下只有一個active的BPServiceActor)

datanode的啟動過程主要完成以下工作:

  • 啟動多種工作執行緒,主要包括:
    • 通訊:BPServiceActor、IpcServer、DataXceiverServer、LocalDataXceiverServer
    • 監控:DataBlockScanner、DirectoryScanner、JVMPauseMonitor
    • 其他:InfoServer
  • 向namenode註冊
  • 初始化儲存結構,包括各資料目錄${dfs.data.dir},及資料目錄下各塊池的儲存結構
  • 【可能】資料塊恢復等(暫不討論)

LazyWriter等特性暫不討論。

文章的組織結構

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

主流程

datanode的Main Class是DataNode,先找DataNode.main():

  public static void main(String args[]) {
    if (DFSUtil.parseHelpArgument(args, DataNode.USAGE, System.out, true)) {
      System.exit(0);
    }

    secureMain(args, null);
  }
  
  ...
  
  public static void secureMain(String args[], SecureResources resources) {
    int errorCode = 0;
    try {
      // 列印啟動資訊
      StringUtils.startupShutdownMessage(DataNode.class, args, LOG);
      // 完成建立datanode的主要工作
      DataNode datanode = createDataNode(args, null, resources);
      if (datanode != null) {
        datanode.join();
      } else {
        errorCode = 1;
      }
    } catch (Throwable e) {
      LOG.fatal("Exception in secureMain", e);
      terminate(1, e);
    } finally {
      LOG.warn("Exiting Datanode");
      terminate(errorCode);
    }
  }
複製程式碼

datanode封裝了非常多工作執行緒,但絕大多數是守護執行緒,DataNode#join()只需要等待BPServiceActor執行緒結束,就可以正常退出(略)。

DataNode.createDataNode():

  public static DataNode createDataNode(String args[], Configuration conf,
      SecureResources resources) throws IOException {
    // 完成大部分初始化的工作,並啟動部分工作執行緒
    DataNode dn = instantiateDataNode(args, conf, resources);
    if (dn != null) {
      // 啟動剩餘工作執行緒
      dn.runDatanodeDaemon();
    }
    return dn;
  }
複製程式碼
  • 在DataNode.instantiateDataNode()執行的過程中會啟動部分工作執行緒(見後)
  • DataNode#runDatanodeDaemon()啟動剩餘的DataXceiverServer、localDataXceiverServer、IpcServer等:
  /** Start a single datanode daemon and wait for it to finish.
   *  If this thread is specifically interrupted, it will stop waiting.
   */
  public void runDatanodeDaemon() throws IOException {
    // 在DataNode.instantiateDataNode()執行過程中會呼叫該方法(見後)
    blockPoolManager.startAll();

    dataXceiverServer.start();
    if (localDataXceiverServer != null) {
      localDataXceiverServer.start();
    }
    ipcServer.start();
    startPlugins(conf);
  }
複製程式碼

回到DataNode.instantiateDataNode():

  public static DataNode instantiateDataNode(String args [], Configuration conf,
      SecureResources resources) throws IOException {
    if (conf == null)
      conf = new HdfsConfiguration();
    
    ... // 引數檢查等
    
    Collection<StorageLocation> dataLocations = getStorageLocations(conf);
    UserGroupInformation.setConfiguration(conf);
    SecurityUtil.login(conf, DFS_DATANODE_KEYTAB_FILE_KEY,
        DFS_DATANODE_KERBEROS_PRINCIPAL_KEY);
    return makeInstance(dataLocations, conf, resources);
  }
複製程式碼

dataLocations維護的是全部${dfs.data.dir},猴子只配置了一個目錄,實際使用中會在將每塊磁碟都掛載為一塊目錄。

從DataNode.makeInstance()開始建立DataNode:

  static DataNode makeInstance(Collection<StorageLocation> dataDirs,
      Configuration conf, SecureResources resources) throws IOException {
    ...// 檢查資料目錄的許可權

    assert locations.size() > 0 : "number of data directories should be > 0";
    return new DataNode(conf, locations, resources);
  }
  
  ...
  
  DataNode(final Configuration conf,
           final List<StorageLocation> dataDirs,
           final SecureResources resources) throws IOException {
    super(conf);
    ...// 引數設定
    
    try {
      hostName = getHostName(conf);
      LOG.info("Configured hostname is " + hostName);
      startDataNode(conf, dataDirs, resources);
    } catch (IOException ie) {
      shutdown();
      throw ie;
    }
  }
  
  ...
  
  void startDataNode(Configuration conf, 
                     List<StorageLocation> dataDirs,
                     SecureResources resources
                     ) throws IOException {
    ...// 引數設定
    
    // 初始化DataStorage
    storage = new DataStorage();
    
    // global DN settings
    // 註冊JMX
    registerMXBean();
    // 初始化DataXceiver(流式通訊),DataNode#runDatanodeDaemon()中啟動
    initDataXceiver(conf);
    // 啟動InfoServer(Web UI)
    startInfoServer(conf);
    // 啟動JVMPauseMonitor(反向監控JVM情況,可通過JMX查詢)
    pauseMonitor = new JvmPauseMonitor(conf);
    pauseMonitor.start();
  
    ...// 略
    
    // 初始化IpcServer(RPC通訊),DataNode#runDatanodeDaemon()中啟動
    initIpcServer(conf);

    metrics = DataNodeMetrics.create(conf, getDisplayName());
    metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
    
    // 按照namespace(nameservice)、namenode的二級結構進行初始化
    blockPoolManager = new BlockPoolManager(this);
    blockPoolManager.refreshNamenodes(conf);
    
    ...// 略
  }
複製程式碼

BlockPoolManager抽象了datanode提供的資料塊儲存服務。BlockPoolManager按照namespace(nameservice)、namenode二級結構組織,此處按照該二級結構進行初始化。

重點是BlockPoolManager#refreshNamenodes():

  void refreshNamenodes(Configuration conf)
      throws IOException {
    LOG.info("Refresh request received for nameservices: " + conf.get
            (DFSConfigKeys.DFS_NAMESERVICES));

    Map<String, Map<String, InetSocketAddress>> newAddressMap = DFSUtil
            .getNNServiceRpcAddressesForCluster(conf);

    synchronized (refreshNamenodesLock) {
      doRefreshNamenodes(newAddressMap);
    }
  }
複製程式碼

命名為重新整理,是因為除了初始化過程主動呼叫,還可以由namespace通過datanode心跳過程下達重新整理命令。

newAddressMap是這樣一個對映:Map<namespace, Map<namenode, InetSocketAddress>>

BlockPoolManager#doRefreshNamenodes():

  private void doRefreshNamenodes(
      Map<String, Map<String, InetSocketAddress>> addrMap) throws IOException {
    assert Thread.holdsLock(refreshNamenodesLock);

    Set<String> toRefresh = Sets.newLinkedHashSet();
    Set<String> toAdd = Sets.newLinkedHashSet();
    Set<String> toRemove;
    
    synchronized (this) {
      // Step 1. For each of the new nameservices, figure out whether
      // it's an update of the set of NNs for an existing NS,
      // or an entirely new nameservice.
      for (String nameserviceId : addrMap.keySet()) {
        if (bpByNameserviceId.containsKey(nameserviceId)) {
          toRefresh.add(nameserviceId);
        } else {
          toAdd.add(nameserviceId);
        }
      }
      
      ...// 略
      
      // Step 3. Start new nameservices
      if (!toAdd.isEmpty()) {
        LOG.info("Starting BPOfferServices for nameservices: " +
            Joiner.on(",").useForNull("<default>").join(toAdd));
      
        for (String nsToAdd : toAdd) {
          ArrayList<InetSocketAddress> addrs =
            Lists.newArrayList(addrMap.get(nsToAdd).values());
          // 為每個namespace建立對應的BPOfferService
          BPOfferService bpos = createBPOS(addrs);
          bpByNameserviceId.put(nsToAdd, bpos);
          offerServices.add(bpos);
        }
      }
      // 然後通過startAll啟動所有BPOfferService
      startAll();
    }
    
    ...// 略
  }
複製程式碼

addrMap即傳入的newAddressMap。Step 3為每個namespace建立對應的BPOfferService(包括每個namenode對應的BPServiceActor),然後通過BlockPoolManager#startAll()啟動所有BPOfferService(實際是啟動所有 BPServiceActor)。

BlockPoolManager#createBPOS()

BlockPoolManager#createBPOS():

  protected BPOfferService createBPOS(List<InetSocketAddress> nnAddrs) {
    return new BPOfferService(nnAddrs, dn);
  }
複製程式碼

BPOfferService.<init>

  BPOfferService(List<InetSocketAddress> nnAddrs, DataNode dn) {
    Preconditions.checkArgument(!nnAddrs.isEmpty(),
        "Must pass at least one NN.");
    this.dn = dn;

    for (InetSocketAddress addr : nnAddrs) {
      this.bpServices.add(new BPServiceActor(addr, this));
    }
  }
複製程式碼

BPOfferService通過bpServices維護同一個namespace下各namenode對應的BPServiceActor。

BlockPoolManager#startAll()

BlockPoolManager#startAll():

  synchronized void startAll() throws IOException {
    try {
      UserGroupInformation.getLoginUser().doAs(
          new PrivilegedExceptionAction<Object>() {
            @Override
            public Object run() throws Exception {
              for (BPOfferService bpos : offerServices) {
                bpos.start();
              }
              return null;
            }
          });
    } catch (InterruptedException ex) {
      IOException ioe = new IOException();
      ioe.initCause(ex.getCause());
      throw ioe;
    }
  }
複製程式碼

逐個呼叫BPOfferService#start(),啟動BPOfferService:

  //This must be called only by blockPoolManager
  void start() {
    for (BPServiceActor actor : bpServices) {
      actor.start();
    }
  }
複製程式碼

逐個呼叫BPServiceActor#start(),啟動BPServiceActor:

  //This must be called only by BPOfferService
  void start() {
    // 保證BPServiceActor執行緒只啟動一次
    if ((bpThread != null) && (bpThread.isAlive())) {
      return;
    }
    bpThread = new Thread(this, formatThreadName());
    bpThread.setDaemon(true); // needed for JUnit testing
    bpThread.start();
  }
複製程式碼

BPServiceActor#start()的執行緒安全性由最外層的BlockPoolManager#startAll()(synchronized方法)保證。

在完成datanode的初始化後,DataNode#runDatanodeDaemon()中又呼叫了一次BlockPoolManager#startAll()。猴子沒明白這次呼叫的作用,但BlockPoolManager#startAll()的內部邏輯保證其只會被執行一次,沒造成什麼壞影響。

主流程小結

在datanode啟動的主流程中,啟動多種重要的工作執行緒,包括:

  • 通訊:BPServiceActor、IpcServer、DataXceiverServer、LocalDataXceiverServer
  • 監控:JVMPauseMonitor
  • 其他:InfoServer

接下來討論BPServiceActor執行緒,它的主要工作是:

  • 向namonode註冊
  • 啟動DataBlockScanner、DirectoryScanner等工作執行緒
  • 儲存結構初始化

BPServiceActor執行緒

在datanode啟動的主流程中,啟動了多種工作執行緒,包括InfoServer、JVMPauseMonitor、BPServiceActor等。其中,最重要的是BPServiceActor執行緒,真正代表datanode與namenode通訊的正是BPServiceActor執行緒。

BPServiceActor#run():

  @Override
  public void run() {
    LOG.info(this + " starting to offer service");

    try {
      while (true) {
        // init stuff
        try {
          // 與namonode握手,註冊
          connectToNNAndHandshake();
          break;
        } catch (IOException ioe) {
          ...// 大部分握手失敗的情況都需要重試,除非丟擲了非IOException異常或datanode關閉
        }
      }

      runningState = RunningState.RUNNING;

      while (shouldRun()) {
        try {
          // BPServiceActor提供的服務
          offerService();
        } catch (Exception ex) {
          ...// 不管丟擲任何異常,都持續提供服務(包括心跳、資料塊彙報等),直到datanode關閉
        }
      }
      runningState = RunningState.EXITED;
    } catch (Throwable ex) {
      LOG.warn("Unexpected exception in block pool " + this, ex);
      runningState = RunningState.FAILED;
    } finally {
      LOG.warn("Ending block pool service for: " + this);
      cleanUp();
    }
  }
複製程式碼

此處說的“通訊”包括與握手、註冊(BPServiceActor#connectToNNAndHandshake)和後期迴圈提供服務(BPServiceActor#offerService(),本文暫不討論)。

啟動過程中主要關注BPServiceActor#connectToNNAndHandshake():

  private void connectToNNAndHandshake() throws IOException {
    // get NN proxy
    bpNamenode = dn.connectToNN(nnAddr);

    // 先通過第一次握手獲得namespace的資訊
    NamespaceInfo nsInfo = retrieveNamespaceInfo();
    
    // 然後驗證並初始化該datanode上的BlockPool
    bpos.verifyAndSetNamespaceInfo(nsInfo);
    
    // 最後,通過第二次握手向各namespace註冊自己
    register();
  }
複製程式碼

通過兩次握手完成了datanode的註冊,比較簡單,不討論。

重點是BPOfferService#verifyAndSetNamespaceInfo():

  /**
   * Called by the BPServiceActors when they handshake to a NN.
   * If this is the first NN connection, this sets the namespace info
   * for this BPOfferService. If it's a connection to a new NN, it
   * verifies that this namespace matches (eg to prevent a misconfiguration
   * where a StandbyNode from a different cluster is specified)
   */
  void verifyAndSetNamespaceInfo(NamespaceInfo nsInfo) throws IOException {
    writeLock();
    try {
      if (this.bpNSInfo == null) {
        // 如果是第一次連線namenode(也就必然是第一次連線namespace),則初始化blockpool(塊池)
        this.bpNSInfo = nsInfo;
        boolean success = false;

        try {
          // 以BPOfferService為單位初始化blockpool
          dn.initBlockPool(this);
          success = true;
        } finally {
          if (!success) {
            // 如果一個BPServiceActor執行緒失敗了,還可以由同BPOfferService的其他BPServiceActor執行緒重新嘗試
            this.bpNSInfo = null;
          }
        }
      } else {
        ...// 如果不是第一次連線(重新整理),則檢查下資訊是否正確即可
      }
    } finally {
      writeUnlock();
    }
  }
複製程式碼

儘管是在BPServiceActor執行緒中,卻試圖以BPOfferService為單位初始化blockpool(包括記憶體與磁碟上的儲存結構)。如果初始化成功,萬事大吉,以後同BPOfferService的其他BPServiceActor執行緒發現BPOfferService#bpNSInfo != null就不再初始化;而如果一個BPServiceActor執行緒初始化blockpool失敗了,還可以由同BPOfferService的其他BPServiceActor執行緒重新嘗試初始化。

DataNode#initBlockPool():

  /**
   * One of the Block Pools has successfully connected to its NN.
   * This initializes the local storage for that block pool,
   * checks consistency of the NN's cluster ID, etc.
   * 
   * If this is the first block pool to register, this also initializes
   * the datanode-scoped storage.
   * 
   * @param bpos Block pool offer service
   * @throws IOException if the NN is inconsistent with the local storage.
   */
  void initBlockPool(BPOfferService bpos) throws IOException {
    ...// 略

    // 將blockpool註冊到BlockManager
    blockPoolManager.addBlockPool(bpos);
    
    // 初步初始化儲存結構
    initStorage(nsInfo);

    ...// 檢查磁碟損壞

    // 啟動掃描器
    initPeriodicScanners(conf);
    
    // 將blockpool新增到FsDatasetIpml,並繼續初始化儲存結構
    data.addBlockPool(nsInfo.getBlockPoolID(), conf);
  }
複製程式碼

此時可知,blockpool是按照namespace逐個初始化的。這很必要,因為要支援Federation的話,就必須讓多個namespace既能共用BlockManager提供的資料塊儲存服務,又能獨立啟動、關閉、升級、回滾等。

DataNode#initStorage()

在逐個初始化blockpool之前,先以datanode整體進行初始化。這一階段操作的主要物件是DataStorage、StorageDirectory、FsDatasetImpl、FsVolumeList、FsVolumeImpl等;後面的FsDatasetImpl#addBlockPool操作的主要物件才會具體到各blockpool。

DataNode#initStorage():

  private void initStorage(final NamespaceInfo nsInfo) throws IOException {
    final FsDatasetSpi.Factory<? extends FsDatasetSpi<?>> factory
        = FsDatasetSpi.Factory.getFactory(conf);
    
    if (!factory.isSimulated()) {
      ...// 構造引數
      // 初始化DataStorage(每個datanode分別只持有一個)。可能會觸發DataStorage級別的狀態裝換,因此,要在DataNode上加鎖
      synchronized (this) {
        storage.recoverTransitionRead(this, bpid, nsInfo, dataDirs, startOpt);
      }
      final StorageInfo bpStorage = storage.getBPStorage(bpid);
      LOG.info("Setting up storage: nsid=" + bpStorage.getNamespaceID()
          + ";bpid=" + bpid + ";lv=" + storage.getLayoutVersion()
          + ";nsInfo=" + nsInfo + ";dnuuid=" + storage.getDatanodeUuid());
    }

    ...// 檢查

    // 初始化FsDatasetImpl(同上,每個datanode分別只持有一個)
    synchronized(this)  {
      if (data == null) {
        data = factory.newInstance(this, storage, conf);
      }
    }
  }
複製程式碼

初始化DataStorage:DataStorage#recoverTransitionRead()

DataStorage#recoverTransitionRead():

  void recoverTransitionRead(DataNode datanode, String bpID, NamespaceInfo nsInfo,
      Collection<StorageLocation> dataDirs, StartupOption startOpt) throws IOException {
    // First ensure datanode level format/snapshot/rollback is completed
    recoverTransitionRead(datanode, nsInfo, dataDirs, startOpt);

    // Create list of storage directories for the block pool
    Collection<File> bpDataDirs = new ArrayList<File>();
    for(StorageLocation dir : dataDirs) {
      File dnRoot = dir.getFile();
      File bpRoot = BlockPoolSliceStorage.getBpRoot(bpID, new File(dnRoot,
          STORAGE_DIR_CURRENT));
      bpDataDirs.add(bpRoot);
    }

    // 在各${dfs.data.dir}/current下檢查並建立blockpool目錄
    makeBlockPoolDataDir(bpDataDirs, null);
    
    // 建立BlockPoolSliceStorage,並放入對映DataStorage#bpStorageMap:`Map<bpid, BlockPoolSliceStorage>`
    BlockPoolSliceStorage bpStorage = new BlockPoolSliceStorage(
        nsInfo.getNamespaceID(), bpID, nsInfo.getCTime(), nsInfo.getClusterID());
    bpStorage.recoverTransitionRead(datanode, nsInfo, bpDataDirs, startOpt);
    addBlockPoolStorage(bpID, bpStorage);
  }
複製程式碼

根據Javadoc,BlockPoolSliceStorage管理著該datanode上相同bpid的所有BlockPoolSlice。然而,猴子暫時沒有發現這個類與升級外的操作有關(當然,啟動也可能是由於升級重啟),暫不深入。

  • BlockPoolSlice詳見後文FsVolumeImpl#addBlockPool。
  • DataStorage#recoverTransitionRead()、BlockPoolSliceStorage#recoverTransitionRead()與資料節點恢復的關係非常大,猴子暫時還沒看懂,以後回來補充。

初始化FsDatasetImpl:FsDatasetFactory#newInstance()

FsDatasetFactory#newInstance():

  public FsDatasetImpl newInstance(DataNode datanode,
      DataStorage storage, Configuration conf) throws IOException {
    return new FsDatasetImpl(datanode, storage, conf);
  }
複製程式碼

FsDatasetImpl.<init>()

  FsDatasetImpl(DataNode datanode, DataStorage storage, Configuration conf
      ) throws IOException {
    ...// 檢查,設定引數等

    @SuppressWarnings("unchecked")
    final VolumeChoosingPolicy<FsVolumeImpl> blockChooserImpl =
        ReflectionUtils.newInstance(conf.getClass(
            DFSConfigKeys.DFS_DATANODE_FSDATASET_VOLUME_CHOOSING_POLICY_KEY,
            RoundRobinVolumeChoosingPolicy.class,
            VolumeChoosingPolicy.class), conf);
    volumes = new FsVolumeList(volsFailed, blockChooserImpl);
    
    ...// 略

    // 每一個Storagedirectory都對應一個卷FsVolumeImpl,需要將這些卷新增到FsVolumeList中
    for (int idx = 0; idx < storage.getNumStorageDirs(); idx++) {
      addVolume(dataLocations, storage.getStorageDir(idx));
    }
    
    ...// 設定lazyWriter、cacheManager等
  }
  
  ...
  
  private void addVolume(Collection<StorageLocation> dataLocations,
      Storage.StorageDirectory sd) throws IOException {
    // 使用`${dfs.data.dir}/current`目錄
    final File dir = sd.getCurrentDir();
    final StorageType storageType =
        getStorageTypeFromLocations(dataLocations, sd.getRoot());

    FsVolumeImpl fsVolume = new FsVolumeImpl(
        this, sd.getStorageUuid(), dir, this.conf, storageType);
    
    ...// 略
    
    volumes.addVolume(fsVolume);
    
    ...// 略
    
    LOG.info("Added volume - " + dir + ", StorageType: " + storageType);
  }
複製程式碼

初始化DataStorage的過程中,將各${dfs.data.dir}放入了storage(即DataNode#storage)。對於datanode來說,${dfs.data.dir}/current目錄就是要新增的卷FsVolumeImpl。

FsDatasetImpl#initPeriodicScanners()

FsDatasetImpl#initPeriodicScanners()(名為初始化,實為啟動):

  private void initPeriodicScanners(Configuration conf) {
    initDataBlockScanner(conf);
    initDirectoryScanner(conf);
  }
複製程式碼

初始化並啟動DataBlockScanner、DirectoryScanners

命名為init或許是考慮到有可能禁用了資料塊和目錄的掃描器,導致經過FsDatasetImpl#initPeriodicScanners方法後,掃描器並沒有啟動。但仍然給人造成了誤解。

FsDatasetImpl#addBlockPool()

FsDatasetImpl#addBlockPool()操作的主要物件具體到了各blockpool,完成blockpool、current、rbw、tmp等目錄的檢查、恢復或初始化:

  public void addBlockPool(String bpid, Configuration conf)
      throws IOException {
    LOG.info("Adding block pool " + bpid);
    synchronized(this) {
      // 向所有卷新增blockpool(所有namespace共享所有卷)
      volumes.addBlockPool(bpid, conf);
      // 初始化ReplicaMap中blockpool的對映
      volumeMap.initBlockPool(bpid);
    }
    // 將所有副本載入到FsDatasetImpl#volumeMap中
    volumes.getAllVolumesMap(bpid, volumeMap, ramDiskReplicaTracker);
  }
複製程式碼

FsVolumeList#addBlockPool()

FsVolumeList#addBlockPool(),併發向FsVolumeList中的所有卷新增blockpool(所有namespace共享所有卷):

  void addBlockPool(final String bpid, final Configuration conf) throws IOException {
    long totalStartTime = Time.monotonicNow();
    
    final List<IOException> exceptions = Collections.synchronizedList(
        new ArrayList<IOException>());
    List<Thread> blockPoolAddingThreads = new ArrayList<Thread>();
    // 併發向FsVolumeList中的所有卷新增blockpool(所有namespace共享所有卷)
    for (final FsVolumeImpl v : volumes) {
      Thread t = new Thread() {
        public void run() {
          try {
            ...// 時間統計
            // 向卷FsVolumeImpl新增blockpool
            v.addBlockPool(bpid, conf);
            ...// 時間統計
          } catch (IOException ioe) {
            ...// 異常處理,迴圈外統一處理
          }
        }
      };
      blockPoolAddingThreads.add(t);
      t.start();
    }
    for (Thread t : blockPoolAddingThreads) {
      try {
        t.join();
      } catch (InterruptedException ie) {
        throw new IOException(ie);
      }
    }
    ...// 異常處理。如果存在異常,僅丟擲掃描捲過程中的第一個異常
    
    ...// 時間統計
  }
複製程式碼

正如FsVolumeList#addBlockPool(),FsVolumeList封裝了很多面向所有卷的操作。

FsVolumeImpl#addBlockPool():

  void addBlockPool(String bpid, Configuration conf) throws IOException {
    File bpdir = new File(currentDir, bpid);
    // 建立BlockPoolSlice
    BlockPoolSlice bp = new BlockPoolSlice(bpid, this, bpdir, conf);
    // 維護FsVolumeImpl中bpid到BlockPoolSlice的對映
    bpSlices.put(bpid, bp);
  }
複製程式碼

BlockPoolSlice是blockpool在每個捲上的實際存在形式。所有捲上相同bpid的BlockPoolSlice組合成小blockpool(概念上即為BlockPoolSliceStorage),再將相關datanode(向同一個namespace彙報的datanode)上相同bpid的小blockpool組合起來,就構成了該namespace的blockpool。

而FsVolumeImpl#bpSlices維護了bpid到BlockPoolSlice的對映。FsVolumeImpl通過該對映獲取bpid對應的BlockPoolSlice,而BlockPoolSlice再反向藉助FsDatasetImpl中的靜態方法完成實際的檔案操作(見後續文章中的寫資料塊過程)。

回到BlockPoolSlice.<init>

  BlockPoolSlice(String bpid, FsVolumeImpl volume, File bpDir,
      Configuration conf) throws IOException {
    this.bpid = bpid;
    this.volume = volume;
    this.currentDir = new File(bpDir, DataStorage.STORAGE_DIR_CURRENT); 
    this.finalizedDir = new File(
        currentDir, DataStorage.STORAGE_DIR_FINALIZED);
    this.lazypersistDir = new File(currentDir, DataStorage.STORAGE_DIR_LAZY_PERSIST);
    // 檢查並建立finalized目錄
    if (!this.finalizedDir.exists()) {
      if (!this.finalizedDir.mkdirs()) {
        throw new IOException("Failed to mkdirs " + this.finalizedDir);
      }
    }

    this.deleteDuplicateReplicas = conf.getBoolean(
        DFSConfigKeys.DFS_DATANODE_DUPLICATE_REPLICA_DELETION,
        DFSConfigKeys.DFS_DATANODE_DUPLICATE_REPLICA_DELETION_DEFAULT);

    // 刪除tmp目錄。每次啟動datanode都會刪除tmp目錄(並重建),重新協調資料塊的一致性。
    this.tmpDir = new File(bpDir, DataStorage.STORAGE_DIR_TMP);
    if (tmpDir.exists()) {
      FileUtil.fullyDelete(tmpDir);
    }
    
    // 檢查並建立rbw目錄
    this.rbwDir = new File(currentDir, DataStorage.STORAGE_DIR_RBW);
    final boolean supportAppends = conf.getBoolean(
        DFSConfigKeys.DFS_SUPPORT_APPEND_KEY,
        DFSConfigKeys.DFS_SUPPORT_APPEND_DEFAULT);
    // 如果不支援append,那麼同tmp一樣,rbw裡儲存的必然是新寫入的資料,可以在每次啟動datanode時刪除rbw目錄,重新協調
    if (rbwDir.exists() && !supportAppends) {
      FileUtil.fullyDelete(rbwDir);
    } // 如果支援append,待datanode啟動後,有可能繼續append資料,因此不能刪除,等待進一步確定或恢復
    
    if (!rbwDir.mkdirs()) {
      if (!rbwDir.isDirectory()) {
        throw new IOException("Mkdirs failed to create " + rbwDir.toString());
      }
    }
    
    if (!tmpDir.mkdirs()) {
      if (!tmpDir.isDirectory()) {
        throw new IOException("Mkdirs failed to create " + tmpDir.toString());
      }
    }
    
    // 啟動dfsUsage的監控執行緒(詳見對hadoop fs shell中df、du區別的總結)
    this.dfsUsage = new DU(bpDir, conf, loadDfsUsed());
    this.dfsUsage.start();
    ShutdownHookManager.get().addShutdownHook(
      new Runnable() {
        @Override
        public void run() {
          if (!dfsUsedSaved) {
            saveDfsUsed();
          }
        }
      }, SHUTDOWN_HOOK_PRIORITY);
  }
複製程式碼

可知,每個blockpool目錄下的儲存結構是在構造BlockPoolSlice時初始化的

關於du的作用及優化:

在linux系統上,該執行緒將定期通過du -sk命令統計各blockpool目錄的佔用情況,隨著心跳彙報給namenode。

執行linux命令需要從JVM繼承fork出子程式,成本較高(儘管linux使用COW策略避免了對記憶體空間的完全copy)。為了加快datanode啟動速度,此處允許使用之前快取的dfsUsage值,該值儲存在current目錄下的dfsUsed檔案中;快取的dfsUsage會定期持久化到磁碟中;在虛擬機器關閉時,也會將當前的dfsUsage值持久化。

ReplicaMap#initBlockPool()

ReplicaMap#initBlockPool(),初始化ReplicaMap中blockpool的對映:

  void initBlockPool(String bpid) {
    checkBlockPool(bpid);
    synchronized(mutex) {
      Map<Long, ReplicaInfo> m = map.get(bpid);
      if (m == null) {
        // Add an entry for block pool if it does not exist already
        m = new HashMap<Long, ReplicaInfo>();
        map.put(bpid, m);
      }
    }
  }
複製程式碼

FsDatasetImpl#volumeMap(ReplicaMap例項)中維護了bpid到各blockpool在該datanode上的所有副本:Map<bpid, Map<blockid, replicaInfo>>

例行挖坑

在以後的文章中,猴子會陸續整理DataNode章的寫資料塊過程、讀資料塊過程,NameNode章、Client章等。

由於猴子也是一步步學習,難免有錯漏之處,煩請讀者批評指正;隨著猴子進一步的學習與自檢,也會隨時更新文章,重要修改會註明勘誤。


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

相關文章