HDFS Decommission节点的长尾分析和问题解决

前言

我们在一个HDFS集群中进行部分节点的Decommission操作。在Decommission过程中,我们发现了如下问题:

  1. Decommission的整个过程进展非常缓慢,一个星期以后,依然有一部分节点无法完成Decommission操作。
  2. 我们尝试通过-refreshNodes重新Decommission,系统没有任何response,这让我们无法确认到底当前的Decommission和我们重新trigger的尝试是成功了还是失败了。
  3. 当我们发现Decommission进展缓慢以后,我们在INFO日志中,无法找到任何与Decommission相关的操作日志。即使我们打开与与Decommission相关的DEBUG日志,也无法从中获取直接跟Decommissiion相关的日志信息,整个过程的诊断非常困难。后来我们发现,这是由于Decommission操作的触发和对Decommission的Block的调度是完全两个相互剥离的异步操作。但是即使设计本身无法改变,我们的想法是,HDFS是否能在日志上有所优化,在对Block进行调度的时候,能否有所提示?

对于 Decommission缓慢的情况,我们采取的措施是:

  • 在不修改代码不变的情况下,调整了部分与Decommission相关的配置以加快Decommission的进行;
  • 我们详细检查了Decommission过程的代码,发现了一个优化点,并且将对应的优化提交给了HDFS社区 TODO。

对于日志问题,我们也向HDFS社区提交了Patch(TODO),这个Patch想解决的问题是:

  • 当用户反复执行-refreshNodes操作,其实只要一个节点正在Decommission,那么后面针对这个节点的所有的-refreshNodes操作都是无效的,会被skip掉。这种关键的日志,能否在日志中体现?毕竟,又有多少用户能在代码层面去详细研究Decommission的详细触发和执行逻辑呢?
  • 当Decommission执行缓慢的时候,能否在DEBUG日志打开的情况下提供给管理员充足的信息以诊断对应原因?

Decommission过慢的分析过程

NameNode页面并不显示Decommission的进度和剩余块数量

首先,在我们发现我们的Decommission操作在过了一周以后依然没有结束,待Decommission的节点依然处于DECOMMISSSIOIN_IN_PROGRESS的状态。我们首先想到的是在NameNode的页面查看这些DataNode上有多少Block已经移走了,有多少Block的Replica还没有移走。
然后,我们从NameNode上看到的是下面的页面,其中,最后一行的DataNode正处在DECOMMISSIONING_IN_PROGRESS状态中:
在这里插入图片描述
我们发现,这个DECOMMISSIONING_IN_PROGRESS的Block数量是一直不变的。所以,我们的问题是,这个数字一直不变,这是否意味着这个节点的Decommission已经没有任何进展了?
后来,我们从代码分析看到,我们从这个页面看到的一个DECOMMISSIONING_IN_PROGRESS节点的块数量,代表的并不是这个节点还需要transfer出去的块的数量,而是这个机器上本身存放的副本数量,与Decommission无关。Decommission操作不会删除这个节点上的任何块数据,而是Decommission操作的发生,导致这个节点上的Replica的状态不再是Live状态,因此,如果不采取任何操作,那么这个待Decommission节点上的Replica对应的Block的副本状态可能就无法满足Redundancy。基于这个考虑,Decommission的过程,其实就是轮询这个机器上的所有块,保证机器在Decommission并且停机以后,这些块的副本数依然满足要求。
这个过程就是Decommission的workload,即块的复制。

增加每次调度的块数量

我们从日志中发现,每一轮调度,只有四个节点被尝试。比如,下面的日志显示,06:18的时候调度4个,然后似乎sleep 了 3s,下一轮调度又是4个节点:

2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795480_54656
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795483_54659
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795486_54662
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795487_54663
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795538_54714
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795547_54723
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795548_54724
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795549_54725

我们查看对应代码,这是由于RedundancyMonitor根据neededReconstruction中的low-redundancy block构造对应的BlockReconstruct的时候,会根据当前集群的Live节点的数量确定这一轮需要考量多少个low-redundancy Block。
RedundancyMonitor会根据集群中当前Live节点的数量乘以一个倍数(dfs.namenode.replication.work.multiplier.per.iteration)得到每轮循环取出的、尝试进行构造BlockReconstructionWork的Block数量。
相邻两轮循环之间RedundancyMonitor会sleep 3s。
在我们的小集群中,我们正在Decommission 5个节点,保留了2个节点,然后有一个节点正在加入到集群中,因此在我们的场景下,每轮(3s钟一轮)仅仅取出4个节点尝试构造BlockReconstructionWork,这个速度是非常低的。
我们将对应的参数调整为10,以增加RedundancyMonitor每轮处理的节点数量:

  <property>
    <name>dfs.namenode.replication.work.multiplier.per.iteration</name>
    <value>10</value>
  </property>

具体原理和代码可以参考我的另一篇文章HDFS块重构和RedundancyMonitor详解中讲解RedundancyMonitor的章节。

增加Stream Limit以避免节点被Skip

在构造BlockReconstructionWork的时候,RedundancyMonitor会首先挑选Source节点。
对于一个Block的所有的source节点,如果一个节点负载过高,那么这个节点就不应该被选择成为Source。显然,如果一个Block的所有Replica所在的节点的负载都很高因此没有被选作Source,那么本轮就不会为这个节点调度BlockReconstructionWork。

NameNode端评价一个DataNode负载是否过高,使用的标准是:已经调度到这个节点上(待调度的(重构任务还没有被DataNode的心跳认领)和已经调度出去(重构任务已经被DataNode的心跳认领)的)的Task的数量是否大于配置的阈值。这两个阈值分别是dfs.namenode.replication.max-streams(默认值是2)和dfs.namenode.replication.max-streams-hard-limit(默认值是4)。其中,dfs.namenode.replication.max-streams其实是一个Soft Limit,即只约束普通优先级的Block,但是不约束QUEUE_HIGHEST_PRIORITY的块的重构。而 dfs.namenode.replication.max-streams-hard-limit属于Hard Limit,也会约束QUEUE_HIGHEST_PRIORITY的块的重构。

在一个大型的HDFS集群中,我们可以使用默认值,因为块分布均匀,每一台机器上所承载的Replica所属的Block都只是集群所有Block中的一小部分。但是在我们只有几台节点的机器上,这些机器上所承载的块很多,并且几乎每一台DataNode机器上的Replica很可能Cover了集群中所有的Block,因此任何一台机器由于负载过高而被排除在source node以外都会导致所有Block的BlockReconstructionWork都无法被选作source nodes。
因此,我们需要将它们设置成一个较大的值:

 <property>
    <name>dfs.namenode.replication.max-streams</name>
    <value>10</value>
  </property>
  <property>
    <name>dfs.namenode.replication.max-streams-hard-limit</name>
    <value>20</value>
  </property>

具体原理和代码可以参考我的另一篇文章HDFS块重构和RedundancyMonitor详解中讲解RedundancyMonitor的章节。

节点被Skip时应该在DEBUG时打印原因

为了找到Decommission缓慢的原因,我们打开了DEBUG日志,发现RedundancyMonitor在尝试为neededReconstructionBlock构造BlockReconstructionWork的时候打印如下日志:

2024-07-02 03:20:54,311 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Block blk_1073810374_69550 cannot be reconstructed from any node

我们分析代码,看到RedundancyMonitor为一个Block调度对应的BlockReconstructionWork发生在方法BlockManager.computeReconstructionWorkForBlocks()中,其基本流程分为3步:

  1. source node的选择:构造对应的BlockReconstructionWork的实现。
    • 对于基于Erasure Coding的和基于Replication的redundancy,BlockReconstructionWork的具体实现类分别是ErasureCodingWork和ReplicationWork。在选择Source node的过程中,涉及到很多不同的选择标准的总和考虑。对于基于Replication 的redundancy,如果我们无法为一个Block选择任何一个符合要求的source node,那么显然这个Reconstruction是无法工作的。
  2. target node的选择:target nodes的选择和我们写文件的过程中为一个Block选择target节点时一模一样的,都是使用的BlockPlacementPolicy接口的具体实现类。其中对于基于Replication和基于Erasure Coding的BlockPlacementPolicy接口实现各不相同,分别是BlockPlacementPolicyDefault和BlockPlacementPolicyRackFaultTolerant,这里不再赘述。
    • 我们打开DEBUG以后,同时从代码中也看到,在选择目标节点过程中是有比较好的DEBUG日志的,详细打印在选择target节点的过程中一个Node因为哪些具体原因被排除。这些原因被定义在了NodeNotChosenReason中:
        private enum NodeNotChosenReason {
          NOT_IN_SERVICE("the node is not in service"),
          NODE_STALE("the node is stale"),
          NODE_TOO_BUSY("the node is too busy"),
          TOO_MANY_NODES_ON_RACK("the rack has too many chosen nodes"),
          NOT_ENOUGH_STORAGE_SPACE("not enough storage space to place the block"),
          NO_REQUIRED_STORAGE_TYPE("required storage types are unavailable"),
          NODE_SLOW("the node is too slow");
      
  3. 当source node和target node都选择成功,还需要对构建的BlockReconstructionWork进行校验,这发生在方法validateReconstructionWork()中。如果校验不通过,同样这个Block的BlockReconstructionWork不会被调度。

所以,一个Low Redundancy Block最后却没有被成功调度BlockReconstructionWork,大致原因有上面的三种情况。但是很可惜,即使打开DEBUG日志,除了target node的选择失败会打印DEBUG日志,其他原因都不会打印日志。
因此我们向HDFS社区提了对应的Issue HDFS-17568,在DEBUG环境下,打印无法为某一个LowRedundancyBlock调度BlockReconstructionWork的具体原因。

在大量节点被Skip的时候加快有效调度

上面讲过,RedundancyMonitor的每一轮调度,都尝试从neededReconstruction中取出固定数量的Block尝试进行调度,每运行一轮,就会sleep 3s才进入下一轮。这个逻辑发生在方法computeBlockReconstructionWork()中:

  int computeBlockReconstructionWork(int blocksToProcess) {
    List<List<BlockInfo>> blocksToReconstruct = null;
    blocksToReconstruct = neededReconstruction
          .chooseLowRedundancyBlocks(blocksToProcess, reset); // 选择最多blocksToProcess个需要进行重构的Block
    return computeReconstructionWorkForBlocks(blocksToReconstruct); // 对这些Block进行重构
  } 

具体流程为:

  1. 选择最多blocksToProcess个需要进行重构的Block,这些Block存放在一个二维数组中,数组的第一维表示这些Blocks调度的优先级,第二维度是对应优先级的Block的列表

    blocksToReconstruct = neededReconstruction
              .chooseLowRedundancyBlocks(blocksToProcess, reset); // 选择最多blocksToProcess个需要进行重构的Block
    
  2. 对这些选定的需要重构的Block进行重构任务的构造和调度

    	return computeReconstructionWorkForBlocks(blocksToReconstruct); // 对这些Block进行重构
    

这里存在一个巨大的问题:通过chooseLowRedundancyBlocks()选定的重构的Block可能大部分由于各种原因(没有合适的source,没有合适的target,validate不通过等)并没有实际调度出BlockReconstructionWork。但是,即使这样,这一轮RedundancyMonitor结束了,要想到下一轮,必须等待3s。这极大拖累了BlockReconstructionWork的调度效率,即可能候选Block在每一轮都是固定的,但是其中有效的、成功的调度可能非常少。

在我们的集群中,我们的Decommission产生的Block在neededReconstruction中的优先级较低,如果有其他高优先级的Block但是这些高优先级的Block总是无法成功调度(比如source节点负载过高而被skip),那么为这些由Decommission带来的Low-Redundancy Block调度BlockReconstructionWork由于优先级很低,会进行无谓的空等过程。这非常像资源预留机制中的饿死现象:可用资源被预留给大应用,小应用却长时间无法获取资源去运行。
我们的优化逻辑是:当遇到无法为其构建BlockReconstructionWork的low-redundancy block时,在当前这一轮调度内,快进检查其他低冗余块并为其安排重建,从而在RedundancyMonitor的一轮运行中,尽量多地进行有效调度。当然,有效调度的块总数将受到参数blocksToProcess的限制。

具体issue和PR请查看HDFS-17569

其他可能改进

  1. JMX中暴露一个节点的under replicated 的Block的数量
    对于一个正在Decommission的节点,是否可以暴露对应的JMX metrics,显示这个节点的under replicate的块数量?这个数量的减少趋势直接反印了这个节点的Decommission的进展。

  2. 能够通过关键词将Decommission的几个大的异步的Stage串联起来

    当Decommission看起来过慢的时候,我们直觉性地通过关键字Decommission搜索NameNode日志,但是很显然,基于我们在下文将会降到的一个节点Decommission的整个过程,由于其基于各种队列的异步特性,尤其是后面的Reconstruction等过程,其实与Decommission并无直接管理。
    尽管这样,我们的问题是,在日志层面我们能否有所优化,比如,虽然,由于往neededReconstruioint中添加Block的原因很多,Decommission只是其中一个,我们是否可以添加辅助DEBUG日志给管理员一些提示,比如这些Block添加进neededReconstruction 的原因,因为Decommission,因为Maintenance,因为用户的手动升副本等等。这样,我们通过比如Decommission搜索,能够搜索到Decommission的全流程的一些进展数据,而不是仅仅是下文所讲的第一个Stage的DEBUG日志。

基本流程解析

下图总结了我们Decommission一个节点的整个过程:
在这里插入图片描述
在忽略具体细节的情况下,我们可以看到,一个节点的Decommissiion是由多个队列、多个线程完成的,而不是一个职责单一、阻塞式等待的过程。这种基于队列和多线程的异步设计,往往对于一个长时间运行的任务的调度是必须的,但是这种设计方式给我们对整个过程的跟踪、调试、日志的分析和理解带来了非常大的麻烦,因为逻辑上的因果关系不再和时间上的因果关系一一对应,事件发生的有序性不再存在,而是以流水线的方式互相重叠。

  • 第一阶段: 当我们通过-refreshNodes触发一个Decommission或者进入Maintenance的操作的时候,将节点的AdminState置为ENTERING_MAINTENANCE或者DECOMMISSION_IN_PROGRESS的状态并将节点加入到pendingNodes以后,触发结束,第一阶段的任务完成。此后如果用户再次通过-refreshNodes重新出发,NameNode仅仅检查发现当前已经处于AdminState的ENTERING_MAINTENANCE或者DECOMMISSION_IN_PROGRESS,就什么也不做就直接退出。
  • 第二阶段:独立线程DatanodeAdminDefaultMonitor从pendingNodes中取出待decommission或者maintenance的节点进行块的扫描和调度。
    • 对节点上的所有Block进行一轮全扫描,全扫描过程中,对于确定由于decommission或者maintenance操作会导致low-redundancy的Block,放入neededReconstruction中待构造ReconstructionWork,同时生成一个low-redundancy的Block的List(insufficientList)。
    • 随后,基于insufficientList中的每一个Block进行剪枝扫描,同样检查将需要加入到neededReconstruction中的Block加入进去,并且由于重构的发生所以副本已经充足因此不会阻塞Decommission的Block从insufficientList中删除。
  • 第三阶段:独立线程RedundancyMonitor从neededReconstruction中取出块,进行Reconstruction的任务调度。任务调度主要需要确认可用的source节点和target节点。这个过程中可能因为各种原因无法调度成功;
  • 第四阶段:DataNode端在收到调度任务以后进行数据的读写和拷贝,并将重构完成的块汇报给NameNode;
  • 第五阶段:收到块汇报的NameNode会重新考量块的副本情况。对于由于重构的发生副本数已经充足的节点,就从neededReconstruction中删除。同时,DatanodeAdminDefaultMonitor在不断地扫描和判定过程中也会发现这个块的副本数已经充足,因此从insufficientList中将该块删除。当一个Node对应的insufficientList被清空,这个Node就可以最终进入Decommission或者Maintenance状态。

用户通过节点刷新触发Decommission

我们通过自定义的 exludes 节点来告诉HDFS我们需要Decommision的节点列表:

  <property>
    <name>dfs.hosts.exclude</name>
    <value>/hadoop/decommissioned-datanodes</value>
  </property>

然后我们通过刷新节点命令,触发对这些节点的decommission操作(参考 Hadoop Official Doc)

$HADOOP_HOME/bin/hdfs dfsdmin -refreshNodes

实际上,是出发了DatanodeManager的refreshDatanodes()方法:

  private void refreshDatanodes() {
    .....
    for (DatanodeDescriptor node : copy.values()) {
      // Check if not include.
      if (!hostConfigManager.isIncluded(node)) {
        node.setDisallowed(true); // 这个Node是显式包含在include中的,因此不允许对这个节点进行操作
      } else { // 只有不在includes中的节点,才有可能进入maintenance或者进入decommission
        long maintenanceExpireTimeInMS =
            hostConfigManager.getMaintenanceExpirationTimeInMS(node);
        if (node.maintenanceNotExpired(maintenanceExpireTimeInMS)) { // maintenance时间还没有到期
          datanodeAdminManager.startMaintenance( // 开始maintenance
              node, maintenanceExpireTimeInMS);
        } else if (hostConfigManager.isExcluded(node)) { //
          datanodeAdminManager.startDecommission(node); // 对于在exclude中的节点执行decommission操作
        } else { //离开maintenance,离开decommission
          datanodeAdminManager.stopMaintenance(node);
          datanodeAdminManager.stopDecommission(node);
        }
      }
      node.setUpgradeDomain(hostConfigManager.getUpgradeDomain(node));
    }
  }

这里涉及到对于节点maintenance和decommision的处理逻辑,最终会交付给DatanodeAdminManager,其基本逻辑为:
2. 在include文件(dfs.hosts.exclude配置文件中的节点列表)中的节点,是绝对不会进入maintenance中或者decommission中的
3. 如果节点不在includes中
4. 如果节点的确进入了maintenance,并且maintenance还没有到期,那么就继续进行maintenance
5. 如果节点在excludes中,那么就通过调用startDecommision()状态
6. 如果都不是,意味着节点不应该处于maintenance状态,也不应该处于decommissiion状态,那么这时候会通过调用stopMaintenance()和stopDecommission()结束这两种状态。

这里不具体讲述maintenance的具体细节,主要关注在decommission的过程:

  @VisibleForTesting
  public void startDecommission(DatanodeDescriptor node) {
    if (!node.isDecommissionInProgress() && !node.isDecommissioned()) {
      // Update DN stats maintained by HeartbeatManager
      hbManager.startDecommission(node);
      // Update cluster's emptyRack
      blockManager.getDatanodeManager().getNetworkTopology().decommissionNode(node);
      // hbManager.startDecommission will set dead node to decommissioned.
      if (node.isDecommissionInProgress()) {
        for (DatanodeStorageInfo storage : node.getStorageInfos()) {
          LOG.info("Starting decommission of {} {} with {} blocks",
              node, storage, storage.numBlocks());
        }
        node.getLeavingServiceStatus().setStartTime(monotonicNow());
        monitor.startTrackingNode(node);
      }
    } else {
      LOG.trace("startDecommission: Node {} in {}, nothing to do.",
          node, node.getAdminState());
    }
  }

从上面代码我们看到:

  1. 如果节点既不是正在decommission,也不是已经完成了decommission,那么就执行decommission的启动工作:

    if (!node.isDecommissionInProgress() && !node.isDecommissioned()) {
          // Update DN stats maintained by HeartbeatManager
          ......
    

    启动Decommission的具体过程为:

    1. 通过HeartbeatManager,将DataNode的AdminStates.DECOMMISSION_INPROGRESS
      	hbManager.startDecommission(node);
      
      其实质上是将这个DatanodeDescriptoradminState置为DECOMMISSION_INPROGRESS状态
      ------------------------------------------------ HeartbeatManager -------------------------------------------
        synchronized void startDecommission(final DatanodeDescriptor node) {
          ....
          node.startDecommission(); // 将DatanodeDescriptor的状态设置为DECOMMISSION_INPROGRESS
        }
      
      -------------------------------------------------- DatanodeInfo ---------------------------------------------------
        public void startDecommission() {
          adminState = AdminStates.DECOMMISSION_INPROGRESS;
        }
      
    2. 维护NetworkTopology中的节点和rack信息,包括将节点从rack的节点列表中删除(如果这是这个rack的最后一个节点,那么会将rack删除),通过将节点加入到decommissionNodes中:
           blockManager.getDatanodeManager().getNetworkTopology().decommissionNode(node); // 在NetworkTopology中卸载节点
      
      NetworkTopology中卸载节点的代码如下:
        --------------------------------------------- NetworkTopology ---------------------------------------------
        public void decommissionNode(Node node) {
        	.....
          decommissionNodes.add(node.getName()); // 将节点加入到decommissionNodes中
          interRemoveNodeWithEmptyRack(node); // 更新对应的节点和rack映射关系
        }
      
    3. 将这个节点添加到DatanodeAdminManager的pendingNodes中。后面会看到,添加到这里的节点会有另外一个独立的线程(DataNodeAdminDefaultMonitor)来单独取出并处理:
      	monitor.startTrackingNode(node);
      
        public void startTrackingNode(DatanodeDescriptor dn) {
          pendingNodes.add(dn);
        }	
      
  2. 如果节点已经处在DECOMMISSION_INPROGRESS或者DECOMMISSIONED状态,只是打印日志,不做任何事情:

    else {
          LOG.trace("startDecommission: Node {} in {}, nothing to do.",
              node, node.getAdminState());
        }
    

    这里涉及到一个关键问题,就是当我们发现我们的Decommission进展速度缓慢,然后企图再次执行hdfs dfsdmin -refreshNodes命令重新出发decommission的时候,实际上HDFS是不会做任何事情的。而且,这是一条TRACE日志,在默认的INFO日志级别下不打印,因此,对于不了解代码的HDFS管理员来说,其实是感到特别迷惑的。我认为这是代码的问题。

PendingNode的处理

DatanodeAdminDefaultMonitor的基本框架

上面讲到,用户通过hdfs dfsadmin -refreshNodes的admin操作启动了对某些节点的decommission或者enter maintenance操作,这些节点进入到DatanodeAdminDefaultMonitor的pendingNodes中。
DatanodeAdminDefaultMonitor是专门用来处理DECOMMISSION_INPROGRESS 和 ENTERING_MAINTENANCE 状态的节点的,它是一个Runnable,会按照指定频率被调度。
在DatanodeAdminManager启动的时候,会加载DatanodeAdminDefaultMonitor实例,然后以固定30s的频率调度该线程:

    Class cls = null;
    cls = conf.getClass(
          DFSConfigKeys.DFS_NAMENODE_DECOMMISSION_MONITOR_CLASS,
          DatanodeAdminDefaultMonitor.class);
    executor.scheduleWithFixedDelay(monitor, intervalSecs, intervalSecs,
        TimeUnit.SECONDS);

DatanodeAdminManager中的pendingNodes只是记录了节点信息,显然,一个节点是否能够进入DECOMMISSIONED状态或者IN_MAINTENANCE状态,必定需要检查这个节点上的所有副本信息,只有无一遗漏的所有副本都满足了某种副本要求,这个节点才会进入DECOMMISSIONED状态或者IN_MAINTENANCE状态。
这是通过

@Override
---------------------------------------- DatanodeDefaultAdminMonitor -------------------------------------
  public void run() {
    .....
    processPendingNodes(); // 处理pendingNodes,放入outOfServiceNodeBlocks中
    check();
  }

DatanodeAdminDefaultMonitor中有一个独立线程,负责做以下两件事情:

  1. 将新加入到pendingNodes中的节点取出,放入到outOfServiceNodeBlocks中。代码很简单,只有一个简单的限流逻辑,即,如果outOfServiceNodeBlocks的大小(代表着正在进行DECOMMISSION或者Enter Maintenance的节点数量)大于maxConcurrentTrackedNodes,那么暂时不会将pendingNodes中的节点加入到outOfServiceNodeBlocks中:

      private void processPendingNodes() {
        while (!pendingNodes.isEmpty() &&
            (maxConcurrentTrackedNodes == 0 ||
                outOfServiceNodeBlocks.size() < maxConcurrentTrackedNodes)) {
          outOfServiceNodeBlocks.put(pendingNodes.poll(), null);
        }
      }
    

    pendingNodes本身是一个PriorityQueue,即从pendingNodes中取出节点时基于比较器有序的,即最近有心跳的节点放在前面,这样,一些最近没有心跳的unhealthy节点的优先级就会较低,即比较晚地放入到outOfServiceNodeBlocks中:

      private final PriorityQueue<DatanodeDescriptor> pendingNodes = new PriorityQueue<>(
          PENDING_NODES_QUEUE_COMPARATOR);
          
      static final Comparator<DatanodeDescriptor> PENDING_NODES_QUEUE_COMPARATOR =
          (dn1, dn2) -> Long.compare(dn2.getLastUpdate(), dn1.getLastUpdate());
    
    
  2. 基于outOfServiceNodeBlocks的数据结构,为这些节点的Block构建对应的ReconstructionWork,并检查Reconstruction的结果,以确定节点是否可以进入最终的DECOMMISSIONED状态或者IN_MAINTENANCE状态。这是在check()方法中完成的:

    ----------------------------------------- DatanodeAdminDefaultMonitor ----------------------------------------
      private void check() {
        final Iterator<Map.Entry<DatanodeDescriptor, AbstractList<BlockInfo>>>
            it = new CyclicIteration<>(outOfServiceNodeBlocks, iterkey).iterator();
        final List<DatanodeDescriptor> toRemove = new ArrayList<>();
        // 每次会检查完outOfServiceNodeBlocks中的所有的DataNode,但是每一轮对于每个节点最多检查的replica数量有上限约束
        while (it.hasNext() && !exceededNumBlocksPerCheck() && namesystem
            .isRunning()) {
          numNodesChecked++;
          final Map.Entry<DatanodeDescriptor, AbstractList<BlockInfo>>
              entry = it.next();
          final DatanodeDescriptor dn = entry.getKey();
           // 获取挂载在这个节点上的所有Block
           AbstractList<BlockInfo> blocks = entry.getValue();
           boolean fullScan = false;
           if (dn.isMaintenance() && dn.maintenanceExpired()) { // 已经进入maintenance,或者maintenance已经到期
             // If maintenance expires, stop tracking it.
             dnAdmin.stopMaintenance(dn);
             toRemove.add(dn);
             continue;
           }
           if (dn.isInMaintenance()) { // 已经进入了maintenance,但是期限还没有到期,处理下一个dn
             continue;
           }
           if (blocks == null) { // 第一次扫描的时候,blocks是空的
             blocks = handleInsufficientlyStored(dn); // 获取这个节点的insufficient的block
             outOfServiceNodeBlocks.put(dn, blocks); // 扫描出来的节点放在outOfServiceNodeBlocks中
             fullScan = true; // 已经完成了full scan,后面不会再进行full scan,而是基于第一次scan的结果进行进一步scan
           } else {
             pruneReliableBlocks(dn, blocks); // 增量处理当前剩余的Blocks
           }
           if (blocks.size() == 0) { // blocks != null 并且已经insufficient的replica已经全部清空
             if (!fullScan) {
               blocks = handleInsufficientlyStored(dn);
               outOfServiceNodeBlocks.put(dn, blocks);
             }
             // If the full scan is clean AND the node liveness is okay,
             // we can finally mark as DECOMMISSIONED or IN_MAINTENANCE.
             final boolean isHealthy =
                 blockManager.isNodeHealthyForDecommissionOrMaintenance(dn); // 节点是健康存活的
             if (blocks.size() == 0 && isHealthy) { // 这个节点的所有replica都已经清空完毕
               if (dn.isDecommissionInProgress()) { // 从DECOMMISSIONING_IN_PROGRESS进入到DECOMMISSIONED状态
                 dnAdmin.setDecommissioned(dn);
                 toRemove.add(dn);
               } else if (dn.isEnteringMaintenance()) { // 从ENTERING_MAINTENANCE进入到IN_MAINTENANCE状态
                 // IN_MAINTENANCE node remains in the outOfServiceNodeBlocks to
                 // to track maintenance expiration.
                 dnAdmin.setInMaintenance(dn);
               }
             } 
           } 
        // Remove the datanodes that are DECOMMISSIONED or in service after
        // maintenance expiration.
        for (DatanodeDescriptor dn : toRemove) {
          outOfServiceNodeBlocks.remove(dn); // decommission 或者 enter maintenance成功,DatanodeAdminDefaultMonitor不再处理
        }
      }
    
    

check()方法的基本流程为:

  • 第一次处理某个Node的时候,check()方法会对这个DataNode进行一轮Block的全扫描,以获取节点上的所有的low-redundancy的replica。在扫描的过程中如果发现这个Replica对应的Block有进行Block Reconstruction的必要,就添加到neededReconstruction中。
  • 全量扫描以后,会进行一轮剪枝扫描。即,以全扫描获取的low-redundancy的replica的List为基础进行减量扫描。即,对这些low-redundancy的replica再次判定是否需要进行Block Reconstruction,如果需要并且当前不在neededReconstruction中,则加入到neededReconstruction中。同时,如果我们发现low-redundancy的replica对应的Block已经有了sufficient redundancy,那么就从list中删除。
  • 通过不断的增量扫描,对于那些已经有了sufficient redundancy的Replica,不断从outOfServiceNodeBlocks中删除掉。
  • 当outOfServiceNodeBlocks中一个Node的所有的Replica都已经被删掉,说明所有的Replica都已经满足了Node进行decommission的要求,这时候节点就可以动DECOMMISSION_IN_PROGRESS状态进入最后的DECOMMISSIONED状态。

为了避免每一次调度处理时间过长,通过参数dfs.namenode.decommission.blocks.per.interval限制每一轮check()所应该处理的最大的块数量,默认是500000

注意,neededReconstruction中存放了需要进行重构的那些Block,其中由于Maintenance或者Decommission而带来的这些LowRedundancy的Block并放入到neededReconstruction中是由DatanodeDefaultAdminMonitor来操作的。但是,neededReconstruction中并不仅仅存放了来自于Decommission或者Maintenance带来的low-redundancy block,而是在所有情况下所有需要进行Reconstruction的Block,都会放入到neededReconstruction中,随后由RedundancyMonitor进行统一检查并构造对应的ReconstructionWork。具体细节请参考我的另一篇文章HDFS块重构和RedundancyMonitor详解
在这里插入图片描述

  1. 构建对outOfServiceNodeBlocks中的nodes的迭代器。其中iterkey是一个全局变量,每次check()方法运行完(因为check()方法完全可能中途结束,不一定每次都能遍历完所有的outOfServiceNodeBlocks中的所有的Datanodes),都会将当前的遍历位置(即当前的节点)更新到iterkey中,下次可以从当前的位置继续,而不是重新从头遍历:

        final Iterator<Map.Entry<DatanodeDescriptor, AbstractList<BlockInfo>>>
            it = new CyclicIteration<>(outOfServiceNodeBlocks,
            iterkey).iterator();
    
  2. 只要当前这一轮的调用的Blocks数量没有超过threadshold,就基于outOfServiceNodeBlocks的迭代器逐个迭代其中的每一个节点:

        while (it.hasNext() && !exceededNumBlocksPerCheck() && namesystem
            .isRunning()) {
    
  3. 对于已经进入Maintenance的节点(AdminStates.IN_MAINTENANCE)并且Maintenance时间已经到期,则进行到期的清理处理,比如将节点从pendingNodes中删除:

           if (dn.isMaintenance() && dn.maintenanceExpired()) { // 已经进入maintenance,或者maintenance已经到期
             // If maintenance expires, stop tracking it.
             dnAdmin.stopMaintenance(dn);
             toRemove.add(dn); // toRemove中的节点会从outOfServiceNodeBlocks中删除
             continue;
           }
    
  4. 对于已经进入Maintenance(AdminStates.IN_MAINTENANCE,不是AdminStates.ENTERING_MAINTENANCE_STATUS)并且还没有到期,不做任何处理:

           if (dn.isInMaintenance()) { // 已经进入了maintenance,但是期限还没有到期,处理下一个dn
             continue;
           }
    
  5. 如果还没有进行full scan,那么先需要进行这个节点的full scan,并置fullScan = true,表示full scan已经进行。full scan会构造这个DN的所有的block的一个迭代器,获取insufficient 的block并放到outOfServiceNodeBlocks中:

    if (blocks == null) { // 第一次扫描的时候,blocks是null,注意和下面第7步的blocks.size == 0区分开
             blocks = handleInsufficientlyStored(dn); // 获取这个节点的insufficient的block
             outOfServiceNodeBlocks.put(dn, blocks); // 扫描出来的节点放在outOfServiceNodeBlocks中
             fullScan = true; // 已经完成了full scan,后面不会再进行full scan,而是基于第一次scan的结果进行进一步scan
           }
    
  6. 如果已经进行了full scan(意思是在前面的某一轮线程调度中已经进行了full scan),那么其实现在要做的其实是一个剪枝(prune)操作,即对当前outOfServiceNodeBlocks中这个DataNode剩余的还没有处理的block进行迭代然后挨个进行副本状态的检查,检查过程中发现这个Block的副本状态已经满足了要求,那么会在迭代过程中把这个block从outOfServiceNodeBlocks中删除

    else {
              pruneReliableBlocks(dn, blocks); // 对这个节点上挂载的这些low-redundancy blocks进行剪枝
            }
    
  7. 再次检查blocks中块的数量,如果发现为0(注意和步骤5中的blocks=null),说明这个DataNode的Blocks已经全部检查并且满足了副本要求因此都在pruneReliableBlocks(…)中删除了。这时候,会在标记节点为DECOMMISSIONED或者IN_MAINTENANCE之前再次来一次全扫描,以防万一:

    if (blocks.size() == 0) {
              if (!fullScan) {  // 
                blocks = handleInsufficientlyStored(dn);
                outOfServiceNodeBlocks.put(dn, blocks);
              }
    
  8. 如果经过二次全扫描,blocks.size依然为0,那么可以尝试让节点进入最终的DECOMMISSIONED或者IN_MAINTENANCE状态

    • 如果节点当前处于 AdminState.DECOMMISSION_IN_PROGRESS状态,那么可以进入AdminState.DECOMMISSIONED
      if (blocks.size() == 0 && isHealthy) {
           if (dn.isDecommissionInProgress()) {
             dnAdmin.setDecommissioned(dn);
             toRemove.add(dn); // 放入到toRemove中,将从outOfServiceNodeBlocks中删除
           }
      
    • 如果节点当前处于AdminState.ENTERING_MAINTENANCE状态,那么可以进入AdminState.IN_MAINTENANCE状态:
      else if (dn.isEnteringMaintenance()) {
                    // IN_MAINTENANCE node remains in the outOfServiceNodeBlocks to
                    // to track maintenance expiration.
                    dnAdmin.setInMaintenance(dn);
      
  9. 任何情况下,都将当前的iterkey更新为当前的Datanode,这样,check()方法的下一次调度就可以从上一次调度的节点开始了

    finally {
            iterkey = dn;
          }
    
  10. 对于unhealthy nodes,比如在DECOMMISSION_INPROGRESS中挂掉的节点,并且现在需要处理的节点(outOfServiceNodeBlocks中除去已经DECOMMISSION或者IN_MAINTENANCE的节点,以及pendingNodes中的节点)已经超过了maxConcurrentTrackedNodes(dfs.namenode.decommission.max.concurrent.tracked.nodes),那么这时候unhealthy nodes最好暂时不要再占用资源了(但是不能放弃track,它们还需要在pendingNodes中或者outOfServiceNodeBlocks中),因此需要将unhealthy nodes中的节点从outOfServiceNodeBlocks中取出来,然后放回到pendingNodes中(上面说过,pendingNodes是一个根据最近的心跳时间的优先级队列,因此没有心跳的unhealthy nodes放入到pendingNodes中以后,相对于健康有心跳的节点,肯定是最后才会取出来)。放回到pendingNodes中是因为这些节点可能在未来的某个时间重新恢复healthy状态。

        int numTrackedNodes = outOfServiceNodeBlocks.size() - toRemove.size(); // outOfServiceNodeBlocks中剩余的节点
        int numQueuedNodes = getPendingNodes().size(); // pendingNodes中的节点,这些节点即将被取出来放入到outOfServiceNodeBlocks
        int numDecommissioningNodes = numTrackedNodes + numQueuedNodes; // 还需要进行check的节点的总数
        if (numDecommissioningNodes > maxConcurrentTrackedNodes) { // 当前待处理的节点已经超过了阈值,因此需要将这些unhealthy节点从outOfServiceNodeBlocks中取出来,避免占用资源
              numDecommissioningNodes, maxConcurrentTrackedNodes, numQueuedNodes);
    
          // Re-queue unhealthy nodes to make space for decommissioning healthy nodes
          getUnhealthyNodesToRequeue(unhealthyDns, numDecommissioningNodes).forEach(dn -> {
            getPendingNodes().add(dn);// 把unhealthyDns节点重新放回到pendingNodes中
            outOfServiceNodeBlocks.remove(dn);  // 把unhealthyDns节点从outOfServiceNodeBlocks中删除,让出资源给正常healthy节点的检查上
          });
        }
    
    
  11. 对于toRemove中的节点(即不再需要在outOfServiceNodeBlocks中进行track的节点,比如,已经进入MAINTENANCE并且MAINTENANCE已经到期的节点,已经进入DECOMMISSIONED的节点,在处理过程中已经发生了异常因此没必要再处理的节点),从outOfServiceNodeBlocks中删除:

        for (DatanodeDescriptor dn : toRemove) {
          Preconditions.checkState(dn.isDecommissioned() || dn.isInService(),
              "Removing node %s that is not yet decommissioned or in service!",
              dn);
          outOfServiceNodeBlocks.remove(dn);
        }
    

对Node上的块进行全扫描以确认副本不足的块

上面讲过,check()方法会首先对outOfServiceNodeBlocks中的新的节点进行全扫描,以确定low-redundancy block。然后基于全扫描获取的low-redundancy block逐一进行Reconstruction的调度。一轮全扫描完成,返回了insufficient redundancy的block list。这个insufficient redundancy block list随后会作为减量Prune扫描的基础。
全扫描是在方法handleInsufficientlyStored()中进行的,该方法输入Datanode,返回这个Datanode的insufficiently replicated的Block列表:

  private AbstractList<BlockInfo> handleInsufficientlyStored(
      final DatanodeDescriptor datanode) {
    AbstractList<BlockInfo> insufficient = new ChunkedArrayList<>();
    processBlocksInternal(datanode, datanode.getBlockIterator(),
        insufficient, false); // 第四个参数是pruneReliableBlocks,即发现副本数是足够的,是否将这个Block从Datanode中删除。全扫描的时候,是false,即只扫描副本数量不足的节点,不负责删除
    return insufficient;
  }

核心方法是processBlocksInternal(),该方法通过传入的一个Block的迭代器,遍历所有的Block,根据Block的副本状态,将replication insufficient的副本放入到insufficientList中:

  private void processBlocksInternal(
      final DatanodeDescriptor datanode,
      final Iterator<BlockInfo> it,
      final List<BlockInfo> insufficientList, // fullscan的时候,这个list没有用到
      boolean pruneReliableBlocks) { // fullscan的时候,pruneReliableBlocks=false
    .....
    while (it.hasNext()) { // 循环遍历每一个Block
      .....
      numBlocksChecked++;
      numBlocksCheckedPerLock++;
      final BlockInfo block = it.next();
      .....

      final BlockCollection bc = blockManager.getBlockCollection(block);
      final NumberReplicas num = blockManager.countNodes(block);
      final int liveReplicas = num.liveReplicas();

      // Schedule low redundancy blocks for reconstruction
      // if not already pending.
      boolean isDecommission = datanode.isDecommissionInProgress();
      boolean isMaintenance = datanode.isEnteringMaintenance();
      boolean neededReconstruction = isDecommission ?
          blockManager.isNeededReconstruction(block, num) : // 有效存活副本(live)并不充足(小于required)
          blockManager.isNeededReconstructionForMaintenance(block, num);
      if (neededReconstruction) {
        if (!blockManager.neededReconstruction.contains(block) &&
            blockManager.pendingReconstruction.getNumReplicas(block) == 0 &&
            blockManager.isPopulatingReplQueues()) {
          // Process these blocks only when active NN is out of safe mode.
          blockManager.neededReconstruction.add(block,
              liveReplicas, num.readOnlyReplicas(),
              num.outOfServiceReplicas(),
              blockManager.getExpectedRedundancyNum(block));
        }
      }
      
      if (dnAdmin.isSufficient(block, bc, num, isDecommission, isMaintenance)) {
        if (pruneReliableBlocks) { // full scan的时候,pruneReliableBlocks = false。
          it.remove(); // 当一个节点的it都删除完了,这个节点就可以进行decommmission了
        }
        continue;
      }
      // isSufficient()返回false
      // We've found a block without sufficient redundancy.
      if (insufficientList != null) {
        insufficientList.add(block); // 加到insufficientList中的block是必须要进行复制,否则就会阻止节点下线的那些block
      }
      ......
  }

该方法的基本流程是:

  1. 通过传入的Iterator,对Block进行遍历:

    while (it.hasNext()) { // 循环遍历每一个Block
    	....
    
  2. 对于当前正在处理的Block,对这个Block的所有的Replica状态进行统计,以便随后判断这个Block的状态以决定是否是replica sufficient:

    	final NumberReplicas num = blockManager.countNodes(block);
    

    一个Block的Replica的状态其实是依赖于这个Replica所在的Datanode的状态决定的,用StoredReplicaState表示。
    需要区分这个Replica的状态StoredReplicaState和节点的状态(AdminState),这里不再详述。
    所有的副本状态被StoredReplicaState表述,顾名思义,StoredReplicaState所表述的副本状态是已经存储下来的副本的整个状态,这些层面的状态一方面是为了给用户的诸如fsck的命令返回副本的统计信息,更重要的,这些状态的统计信息,将用来决定下一步对副本是否需要进行重构的策略,比如副本数是否太低,太低的副本需要进行重构,副本数是否太高,副本数太高的副本需要进行部分的删除。

    public enum StoredReplicaState {
        // live replicas. for a striped block, this value excludes redundant
        // replicas for the same internal block
        LIVE,
        READONLY,
        // decommissioning replicas. for a striped block ,this value excludes
        // redundant and live replicas for the same internal block.
        DECOMMISSIONING,
        DECOMMISSIONED,
        // We need live ENTERING_MAINTENANCE nodes to continue
        // to serve read request while it is being transitioned to live
        // IN_MAINTENANCE if these are the only replicas left.
        // MAINTENANCE_NOT_FOR_READ == maintenanceReplicas -
        // Live ENTERING_MAINTENANCE.
        // 当 一个节点处于ENTERING_MAINTENANCE中(还没到达最终的IN_MAINTENANCE), 这个节点上的internal block如果没有其它副本,
        // 那么这个node还是会接着serve 这个replica 的读请求。显然,当节点进入到IN_MAINTENANCE中的时候,读请求就不会过来了,
        // 因此进入MAINTENANCE_NOT_FOR_READ
        MAINTENANCE_NOT_FOR_READ,
        // Live ENTERING_MAINTENANCE nodes to serve read requests.
        MAINTENANCE_FOR_READ,
        CORRUPT,
        // excess replicas already tracked by blockmanager's excess map
        EXCESS, // 副本数量超过了要求,这些超过要求的replica 最终会被删除
        STALESTORAGE,
        // for striped blocks only. number of redundant internal block replicas
        // that have not been tracked by blockmanager yet (i.e., not in excess)
        REDUNDANT
      }
    
    
    

    副本的状态StoredReplicaState是对连续布局和条带布局统一而言的,并不是专指某种布局方式。但是,某些特定状态在两种布局模式下的含义稍有不同。副本每一个状态的具体含义,我们可以从BlockManager构造NumberReplicas对象的方法checkReplicaOnStorage()清楚地看到:

    private StoredReplicaState checkReplicaOnStorage(NumberReplicas counters,
          BlockInfo b, DatanodeStorageInfo storage,
          Collection<DatanodeDescriptor> nodesCorrupt, boolean inStartupSafeMode) {
        final StoredReplicaState s;
        if (storage.getState() == State.NORMAL) {
          final DatanodeDescriptor node = storage.getDatanodeDescriptor();
          if (nodesCorrupt != null && nodesCorrupt.contains(node)) {
            s = StoredReplicaState.CORRUPT;
          } else if (inStartupSafeMode) {
            s = StoredReplicaState.LIVE;
            counters.add(s, 1);
            return s;
          } else if (node.isDecommissionInProgress()) {
            s = StoredReplicaState.DECOMMISSIONING;
          } else if (node.isDecommissioned()) {
            s = StoredReplicaState.DECOMMISSIONED;
          } else if (node.isMaintenance()) { // 当节点处于ENTERING_MAINTENANCE或者IN_MAINTENANCE的状态时
            if (node.isInMaintenance() || !node.isAlive()) { // 如果节点已经处于IN_MAINTENANCE,或者尽管还处于ENTERING_MAINTENANCE,但是节点节点已经死掉了,因此数据不可读,因此副本状态标记为MAINTENANCE_NOT_FOR_READ
              s = StoredReplicaState.MAINTENANCE_NOT_FOR_READ;
            } else { //其他情况,比如节点存活并且处于ENTERING_MAINTENANCE
              s = StoredReplicaState.MAINTENANCE_FOR_READ;
            }
          } else if (isExcess(node, b)) {
            s = StoredReplicaState.EXCESS; // 超出正常要求的副本数
          } else {
            s = StoredReplicaState.LIVE; // 没有超出正常要求的副本数
          }
          counters.add(s, 1);
          // //如果这个Storage是stale storage,那么,认为这个replica是stale状态,直到收到对应DN的heartbeat
          if (storage.areBlockContentsStale()) {
            counters.add(StoredReplicaState.STALESTORAGE, 1);
          }
        } else if (!inStartupSafeMode &&
            storage.getState() == State.READ_ONLY_SHARED) {
          s = StoredReplicaState.READONLY;
          counters.add(s, 1);
    

    副本的大部分状态是由这个副本所在的存储状态决定的:

    • LIVE: 就是我们最常见的正常的存活状态。
      • 对于条带布局模式,虽然预期是每一个internal block只有一个副本,但是有可能存在某个internal block同时存在LIVE状态和其它状态,在这种情况下,LIVE状态是排他的,即,这个Internal Block只要有一个副本是LIVE,那么我们就认为这个Internal Block的状态是LIVE。关于节点状态的一些去重操作,参考BlockManager.countLiveAndDecommissioningReplicas()
      • 从代码可以看到,对于与DECOMMISSION或者MAINTENANCE相关的状态,即使此时机器还在线,副本可读,副本状态并不会是LIVE,而是对应的DECOMMISSION或者MAINTENANCE的相关状态。
    • READONLY: 如果replica所在的Storage的类型是StorageType.State.READ_ONLY_SHARED, 那么这个副本的状态就是READONLY。这个READ_ONLY_SHARED是一个特殊的HDFS特性,我看了一下对应的HDFS issue HDFS-5318(issue里面有对应的design doc, 感兴趣的读者可以看看),其大致意思就是将我们传统的通过物理磁盘存储block的方式转移到通过共享存储(NAS, S3等)存储块信息,由于不再存放在某台DataNode上,因此客户端可以通过任意DataNode读取到这个Block。这个READONLY状态和本文的关系不大,不做详细解释。
    • DECOMMISSIONING: 一个机器decommision的过程就意味着上面的replica需要全部转移(copy)到其它机器上,在全部转移完成以前,这个机器上的block都是DECOMMISSIONING的状态。显然,对于一个条带布局的 internal block,如果这个块已经成功转移到其它的Live的机器上,那么这同一个internal block就会在NameNode端同时存在LIVE和DECOMMISSIONING的状态,这时候,NameNode的判定状态是Live状态。关于节点状态的一些去重操作,参考BlockManager.countLiveAndDecommissioningReplicas()
    • DECOMMISSIONED: 副本所在的机器已经decommission结束。
    • MAINTENANCE_FOR_READ: 关于机器的maintenance状态,感兴趣的读者可以自行学习,它发生在我们需要暂时将某个节点进行下线或者升级同时又不希望这个节点的短暂下线引起集群大量的副本拷贝的场景。与maintenance相关的状态与读写的关系是:
      • 凡是与Maintenance相关的状态(ENTERING_MAINTENANCE或者IN_MAINTENANCE状态),机器都不可能再服务写请求
      • 处于ENTERING_MAINTENANCE状态并且依然存活的机器,是可以服务读请求的;
      • 一个机器可能在ENTERING_MAINTENANCE的过程中死亡,这时候显然上面的所有副本也是不可读的;
      • 一个机器一旦真正进入了IN_MAINTENANCE状态,无论是否存活,都不会再server任何请求,包括读请求。因为我们将一个机器进入MAINTENANCE的目的大部分都是希望机器短暂停机维修等。
      • 所以,只有当一个机器处于ENTERING_MAINTENANCE并且存活,它上面的副本状态才会是MAINTENANCE_FOR_READ
    • MAINTENANCE_NOT_FOR_READ:从上面的分析可以看到,如果一个机器已经进入到IN_MAINTENANCE状态,或者这个机器在ENTERING_MAINTENANCE的状态中死亡,这时候这个机器将拒绝任何请求,包括读请求。
    • CORRUPT: 这个replica所在的机器存储已经CORRUPT。请注意区分Block corrupt和Replica corrupt的区别,当且仅当一个Block的所有的replica都已经corrupt了,那么这个Block会被认为是corrupt。NameNode端corrupt的replica都被一个叫做CorruptReplicasMap的对象管理,存放了所有的corrupted的replica以及对应的Corrupt的原因:
      public class CorruptReplicasMap{
       /** The corruption reason code */
       public enum Reason {
         NONE,                // not specified.
         ANY,                 // wildcard reason
         GENSTAMP_MISMATCH,   // mismatch in generation stamps
         SIZE_MISMATCH,       // mismatch in sizes
         INVALID_STATE,       // invalid state
         CORRUPTION_REPORTED  // client or datanode reported the corruption
       }
       // 存放了所有corrupt的replica
       private final Map<Block, Map<DatanodeDescriptor, Reason>> corruptReplicasMap =
         new HashMap<Block, Map<DatanodeDescriptor, Reason>>();
      
      在BlockManager中维护了CorruptReplicasMap的引用,所有corrupt replica都是通过BlockManager.markReplicaAsCorrupt()来添加到corruptReplicas中的,主要有以下情况:
      1. 来自DataNode的增量块汇报(DatanodeProtocol.blockReceivedAndDelete接口)或者全量块汇报(DatanodeProtocol.blockReport)接口。在收到这些块汇报以后,NameNode会对所有汇报上来的块进行时间戳和size的检测,如果发现汇报上来的块和自己在blockMap中存储的块的时间戳或者size不一致,则认定该块是corrupt块
      2. 来自DataNode直接汇报的badBlock(通过DatanodeProtocol.reportBadBlocks()),NameNode收到这些会报上来的badBlock会直接通过调用markBlockAsCorrupt()将其标记为corrupt block。DataNode在什么情况下会直接上报badBlocks呢?这主要发生在比如DataNode被分配了reconstruct的任务时,会从远程读取一些replica进行重构,这时候如果读取发生问题,就会认为是corrupted replica,然后通过DatanodeProtocol.reportBadBlocks()接口上报NameNode。
      3. 客户端在读取块的过程中发现块的校验失败,会通过(ClientProtocol.reportBadBlocks()接口)告知NameNode
      4. DataNode在完成了某个Recovery以后,通过DataNodeProtocol.commitBlockSynchronization()接口告知NameNode,NameNode会在适当情况下将replica标记为corrupt
    • STALESTORAGE:这种状态其实是一种Corner Case。当NameNode发生重启或者Failover(从standby到达active状态)发生,NameNode会将所有的DataNode 的所有的Storage标记为Stale(陈旧)状态,这时候这些storage上的replica也全部为stale状态,直到收到了对应的DataNode发送过来的关于这个Storage的心跳信息,才会解除Stale状态。Stale状态是为了处理在NameNode发生状态转移的时候DataNode和新的NameNode发生的一些不一致状态。在Stale状态的副本的大多数处理都会延迟,因为这是一个中间状态,比如我们发现一个replica的副本数过多,但是其中有一个副本是STALESTORAGE状态,那么这时候我们不可以贸然去删除多余副本,因为这时候有可能对应的 DataNode已经将副本删除,等汇报上来的时候,NameNode也将副本在另外机器上删除,造成副本丢失。
    • REDUNDANT: 只适用于条带布局,某一个internal block的副本数量多余一个。
    • EXCESS: 含义其实于REDUNDANT,只不过EXCESS指的是NameNode已经发现了该replica有多余副本(比如通过fsck,或者通过RedundancyMonitor的定时扫描线程),同时将这个replica的信息放到excessRedundancyMap中去,所有放到excessRedundancyMap中的internal block都会采用响应策略删除一个replica的多余副本。
      从上面的状态分析可以看到,副本的状态和一些存储的状态有一部分是用户操作的,比如Decommission和Maintenance,有些是机器自己汇报的,比如CORRUPT, 有些是集群本身的状态发展而成的,比如LIVE, STALESTORAGE, REDUNDANT, EXCESS
  3. 在统计了这个Block的所有Replica的状态并存放在对象NumberReplicas中以后,就开始判断节点是否需要进行Reconstruction。如果需要,则添加到neededReconstruction中:

      boolean isDecommission = datanode.isDecommissionInProgress();
      boolean isMaintenance = datanode.isEnteringMaintenance();
      boolean neededReconstruction = isDecommission ?
          blockManager.isNeededReconstruction(block, num) : // 有效存活副本(live)并不充足(小于required)
          blockManager.isNeededReconstructionForMaintenance(block, num);
      if (neededReconstruction) {
        if (!blockManager.neededReconstruction.contains(block) &&
            blockManager.pendingReconstruction.getNumReplicas(block) == 0 &&
            blockManager.isPopulatingReplQueues()) {
          // 需要进行Reconstruction,添加到neededReconstruction中
          blockManager.neededReconstruction.add(block,
              liveReplicas, num.readOnlyReplicas(),
              num.outOfServiceReplicas(),
              blockManager.getExpectedRedundancyNum(block));
        }
      }
    

    我们后面将详细讲解isNeededReconstruction()方法和isNeededReconstructionForMaintenance()方法的判断过程。

  4. 如果我们发现对应的Block的副本数量足够了,并且当前处于prune的过程,就将对应的block从iterator中删除,即从iterator对应的列表中删除:

       if (dnAdmin.isSufficient(block, bc, num, isDecommission, isMaintenance)) {
         if (pruneReliableBlocks) { // full scan的时候,pruneReliableBlocks = false。
           it.remove(); // 当一个节点的it都删除完了,这个节点就可以进行decommmission了
         }
         continue;
       }
    

    我们从调用者check()方法可以看到,一个Node最终能够进入IN_MAINTENANCE或者最终进入DECOMMISSIONED状态的标准是: outOfServiceNodeBlocks中对应的Node下面挂载的所有的Block的副本状态满足了指定要求。这里的指定要求就是dnAdmin.isSufficient()返回true,这里的it.remove()操作就是将这个Block从outOfServiceNodeBlocks中对应的Node下面删除。

    从上面的逻辑可是看出来,将一个块加入到neededReconstruction,和将这个块从Iterator中移除是两个独立的逻辑。我们的疑问是,难道不应该是,只要一个块加入到了neededReconstruction中,就不应该从Iterator中移除吗?并不是这样的。仔细分析dnAdmin.isSufficient()的代码和比如isNeededReconstruction()以及isNeededReconstructionForMaintenance的代码可以看到,下面的两种情况,这些Block需要进行Reconstruction,但是已经不再会阻碍Datanode的decommission和maintenance了:

    • 虽然没有满足当前块期望的存活的冗余的要求,但是已经满足整个系统的默认的冗余要求(比如,当前块设置的副本数是5,系统默认为3,那么,如果当前块的副本数是4,显然会进行reconstruction,但是,Reconstruction不会阻碍DataNode的decommission/maintenance)
    • 虽然满足了当前块期望的存活的冗余的要求,但是整个放置不满足要求。这种情况下,显然会调度对应的Reconstruction,但是同样的,Reconstruction不会阻碍DataNode的decommission/maintenance。
  5. 否则,则将对应的Block添加到insufficientList中。这显然发生在full scan的时候,因为full scan的时候insufficientList不为空,因此将full scan获取的副本不足的Block放入insufficientList中。

          if (insufficientList != null) {
            insufficientList.add(block); // 加到insufficientList中的block是必须要进行复制,否则就会阻止节点下线的那些block
          }
    

这就是processBlocksInternal()的基本流程。通过传入到迭代器对Block进行迭代,迭代过程中获取这个Block的所有的Replica的状态统计信息,根据副本状态的统计信息,对于需要进行Reconstruct的Block,将节点加入到neededReconstruction中。

基于全量扫描的结果进行Prune扫描

对于某个DataNode的全量扫描结束以后,生成了一个Insufficient Redundancy Block List,这个list中的Block都是副本数量不足(isSufficient() == False)的Block,只要DataNode上还有任何一个Block的副本数不足,DataNode就不可以进入Maintenance或者Decommissioned状态。增量扫描就是:

  1. 扫描这个Insufficient Redundancy Block List,如果这个List中依然有Block需要进行Reconstruction但是还没有进行Reconstruction,就对其调度ReconstructionWork。
  2. 见着这里的Block的状态是否已经从Insufficient Redundancy 中改出,如果改出,就从List中删除

增量剪枝扫描和上一节的全量扫描都调用的是processBlocksInternal()方法,只不过参数不同:

  1. 全量扫描handleInsufficientlyStored()的时候,Block的迭代器Iterator<BlockInfo> it是这个DataNode上所有的Block的List的迭代器,而增量剪枝的时候这个迭代器是全量扫描所获取的low-redundancy的block的list或者上一次增量剪枝扫描所剩下的Block List所形成的迭代器,即outOfServiceNodeBlocks中这个Datanode的Block List的迭代器,代表了需要进行检查和处理的low-redundancy的block。

      // 全量prune扫描
      private AbstractList<BlockInfo> handleInsufficientlyStored(
          final DatanodeDescriptor datanode) {
        AbstractList<BlockInfo> insufficient = new ChunkedArrayList<>();
        processBlocksInternal(datanode, datanode.getBlockIterator(),
            insufficient, false); // 第四个参数是pruneReliableBlocks,即发现副本数是足够的,是否将这个Block从Datanode中删除。全扫描的时候,是false,即只扫描副本数量不足的节点,不负责删除
        return insufficient;
      }
    
      // 增量prune扫描
      private void pruneReliableBlocks(final DatanodeDescriptor datanode,
                                       AbstractList<BlockInfo> blocks) {
        processBlocksInternal(datanode, blocks.iterator(), null, true);
      }
    
  2. 全扫描的时候,第三个参数insufficientList传入的是一个初始化的List,这样,全扫描以后的结果会放入到insufficientList中,即全扫描所获取的low-redundancy的结果。而增量剪枝扫描的事后,insufficientList是空的,因为是直接传入Iterator<BlockInfo> it的,扫描过程中对于已经replication sufficient的block会直接通过这个迭代器指针从List中删除。这个从上面的代码能够看出来。

  3. 全量扫描的时候,pruneReliableBlocks为false,即如果发现一个Block是 replication sufficient,不执行删除,因此此时扫描的迭代器是来自于Datanode的所有block组成的list。而增量剪枝扫描时pruneReliableBlocks为true,即如果发现一个Block是 replication sufficient,则进行prune,即认为这个Block已经通过Reconstruction进入到了副本充足的状态,因此从outOfServiceNodeBlocks中删除。

      if (dnAdmin.isSufficient(block, bc, num, isDecommission, isMaintenance)) {
        if (pruneReliableBlocks) { // full scan的时候,pruneReliableBlocks = false。
          it.remove(); // 当一个节点的it都删除完了,这个节点就可以进行decommmission了
        }
        continue;
      }
    

是否需要进行Reconstruction的判断过程

上面说过,如果我们发现一个Replica所在的机器处于DECOMMISSIONING或者ENTERING_MAINTENANCE状态,那么就需要判断这个Replica对应的Block是否需要进行Reconstruction以扩充副本:

      boolean isDecommission = datanode.isDecommissionInProgress();
      boolean isMaintenance = datanode.isEnteringMaintenance();
      boolean neededReconstruction = isDecommission ?
          blockManager.isNeededReconstruction(block, num) : // 有效存活副本(live)并不充足(小于required)
          blockManager.isNeededReconstructionForMaintenance(block, num);
DECOMMISSIONING状态下是否需要进行Reconstruction

对于一个正在DECOMMISSIONING的节点,其实是调用isNeededReconstruction()方法:

  boolean isNeededReconstruction(BlockInfo storedBlock,
      NumberReplicas numberReplicas) {
    // 不考虑pending的replica,这个block的存活replica的数量大于等于期望的副本数量(块的副本数)
    return isNeededReconstruction(storedBlock, numberReplicas, 0);
  }

可以看到,当且仅当这个块是一个COMPLETE的块(不是一个正在写入的块或者损坏的块),并且没有足够的有效Replica,那么这个Block就是一个需要Reconstruction的Block:

  boolean isNeededReconstruction(BlockInfo storedBlock,
      NumberReplicas numberReplicas, int pending) {
    return storedBlock.isComplete() &&
        !hasEnoughEffectiveReplicas(storedBlock, numberReplicas, pending);
  }

所以,核心方法在于hasEnoughEffectiveReplicas()

  // 如果 live + pending replicas 的数量不小于所需要的replica数量,并且(还有pending的replica,或者没有pending的并且已经满足放置策略)
  // 这里的含义是,如果有pending的,那么我们先不用考虑是否满足placement policy
  boolean hasEnoughEffectiveReplicas(BlockInfo block,
      NumberReplicas numReplicas, int pendingReplicaNum) {
    int required = getExpectedLiveRedundancyNum(block, numReplicas); // 先看看需要多少个live的副本
    int numEffectiveReplicas = numReplicas.liveReplicas() + pendingReplicaNum;
    return (numEffectiveReplicas >= required) &&
        (pendingReplicaNum > 0 || isPlacementPolicySatisfied(block));
  }

从hasEnoughEffectiveReplicas()方法可以看到,如果一个块的有效replica(Effective Replica)的数量不小于所期望的存活Replica的数量,并且这个块的放置状态也满足要求,或者尽管放置状态不满足要求但是还有pending的replica(没有来得及汇报的replica,也许这个汇报上来的replica会让放置状态从不满足要求变为满足要求),那么我们认为这个Block有足够的有效replica(Effective Replica)。反之则认为没有足够的有效replica(Effective Replica)。
那么,一个块的所期望的存活Replica是怎么计算的呢?这是在方法getExpectedLiveRedundancyNum()中计算的:

  // 期望的最少的live replica的数量
  public short getExpectedLiveRedundancyNum(BlockInfo block,
      NumberReplicas numberReplicas) {
    // 对于striped block,expectedRedundancy 数量指的是data block + parity block的数量,
    // 对于replication,expectedRedundancy指的是replication factor
    final short expectedRedundancy = getExpectedRedundancyNum(block);
    // 假如当前我的block配置的副本是5, 有2个副本是处于maintenance,那么我期待的live 副本数量是5-2=3
    return (short)Math.max(expectedRedundancy -
        numberReplicas.maintenanceReplicas(), // 处于maintenance中的replica也算是live,这本身就是maintenance的目的,让replica短时间可以容忍丢失,但是处于decommission的不能算live
        // 最小的maintenance 副本数量。对于striped block,最小的maintenance副本数量就是data block 的数量,说明对于
        // stripe block, 不允许data block丢失
        getMinMaintenanceStorageNum(block));
  }

从上面的getExpectedLiveRedundancyNum()方法可以看到

  • 如果一个Block的所有Replica没有任何一个有诸如IN_MAINTENANCE或者DECOMMISSIONED等特殊状态,那么期望的存活副本数就是系统配置的Replication Factor(Erasure Coding又是另外的计算方式,具体可以参考 TODO这篇文章)。

  • 如果有节点处于IN_MAINTENANCE状态,那么会进行一些特殊处理。即,如果有的Replica所在的节点处于MAINTENANCE并且节点正常存活,那么期望的节点需要减去处于MAINTENANCE状态的副本数量,或者,等于最小的MAINTENANCE副本数量(dfs.namenode.maintenance.replication.min所配置,默认是1),两者的较大值。

    这里,我们梳理了一下上述方法所涉及到的副本的一些状态和数量定义:

    • 什么是待定副本数(Pending Replica): 就是我们讲的PendingReconstructionBlocks对象维护的replica -> targets信息,即一个block写完(COMMITTED或者COMPLETED)但是NameNode还没收到足够的DataNode汇报,那么预期还需要收到多少个DataNode的汇报的数量。比如3副本的Block,NameNode目前只收到1个DataNode的汇报,那么这个Block的pendingReplicaNum是2。
    • 什么叫有效副本数量(Effective Replicas):从方法hasEnoughEffectiveReplicas()的代码 int numEffectiveReplicas = numReplicas.liveReplicas() + pendingReplicaNum;,即存活状态的副本数量再加上待定状态的副本数量。存活状态的副本是有效副本很容易理解,但是为什么待定的副本也算作有效副本呢?因为待定的副本是正常状态下还缺失的待汇报的数量,这种缺失是很正常的,正常情况下只需要再等等就可以,因此,待定的副本也算作有效。
    • 什么叫允许进入Maintenance状态的最小存活副本数:即要想将某一个replica所载的DataNode进入maintenance状态,那么这个DataNode上的每一个Block需要多少个存活的副本?这是为了避免某个节点进入maintenance状态造成了某些数据不可读的状态。从方法getMinMaintenanceStorageNum()可以看到,对于纠删码,如果进入ENTERING_MAINTENANCE状态以后,依然最少有dataBlockNum个副本存活,那么这个replica就可以进入Maintenance状态。对于3副本,这个默认值是1,即ENTERING_MAINTENANCE的节点中,如果所有的Block都至少有一个存活的副本,那么这个Datanode就可以正式进入MAINTENANCE状态。这就是为什么StoredReplicaState在进入正式的MAINTENANCE状态以前,有一个ENTERING_MAINTENANCE状态,在ENTERING_MAINTENANCE状态时,DataNode会有对应的Monitor确认所有的Block的live replica数量都大于getMinMaintenanceStorageNum(),才会从ENTERING_MAINTENANCE进入到IN_MAINTENANCE状态,避免进入到IN_MAINTENANCE状态(进入IN_MAINTENANCE状态以后的节点很可能出现短时不可服务,比如短暂的关机维修升级等)以后导致数据副本缺失。
    • 当前期望的存活副本数量(Expected Live Redundancy): 从getExpectedLiveRedundancyNum()方法的return (short)Math.max(expectedRedundancy - numberReplicas.maintenanceReplicas(),getMinMaintenanceStorageNum(block));代码可以看到,不考虑特殊情况,对于连续布局,期望的副本数量就是配置的副本数,对于纠删码,期待的副本数量就是dataBlocksNum + parityBlocksNum, 比如RS(6,2)中等于8。但是,由于有些Block的部分副本有可能处于maintenance状态,这些副本虽然暂时不可用,但是并没有丢失,因此,期待的副本数量应该减去这些处于Maintenance状态的副本。比如RS(6,2)中,有3个副本都处于maintenance状态,那么我期待的存活副本数量是8-3=5。同时期望的存活副本数量应该不小于允许进入maintenance的最小存活副本数量。
    • 什么叫足够多的有效Replica: 即当前有效的副本数量不小于期望的副本数量,并且当前还有pending的副本,或者虽然没有pending的副本,但是整个Block的replica分布处于满足PlacementPolicy要求的状态。
      • 这意味着如果有效的副本数量不小于期望的副本数量,并且还有pending的副本,这时候即使整个Block的副本不满足 PlacementPolicy要求的状态,也认为有足够多的有效Replica,这样判定是因为存在pending的副本,所以认为极有可能当pending的副本的DataNode汇报上来以后,PlacementPolicy就会被满足。
MAINTENANCE状态下是否需要进行Reconstruction

对于一个处于ENTERING_MAINTENANCE的节点,通过方法isNeededReconstructionForMaintenance()来判定是否需要进行Reconstruction:

  boolean isNeededReconstructionForMaintenance(BlockInfo storedBlock,
      NumberReplicas numberReplicas) {
    return storedBlock.isComplete() && (numberReplicas.liveReplicas() <
        getMinMaintenanceStorageNum(storedBlock) || // 默认是1
        !isPlacementPolicySatisfied(storedBlock));
  }

和DECOMMISSIONING一样,Reconstruction也仅仅是针对已经COMPLETED的Block,对于还处在构造中或者仅仅是COMMITTED或者CORUPTTED等等其他状态的Block不在考虑范畴。

从isNeededReconstructionForMaintenance()方法得知,一个Block需要进行Reconstruction的条件是:

  • 这个节点是COMPLETED状态
    并且
  • 满足以下要求:
    • 这个节点的Live Replica的数量已经小于所配置的允许进入MAINTENANCE的最小Live副本数量(dfs.namenode.maintenance.replication.min所配置,默认是1)
      或者
    • 虽然Live Replica的数量已经满足了允许进入MAINTENANCE的最小Live副本数量,但是不满足副本放置策略

综上所述,processBlocksInternal()方法如果发现一个Datanode处于IN_MAINTENANCE或者DECOMMISSION_IN_PROGRESS,并且发现对应的块(这个块有副本在这台机器上)需要进行Reconstruction,那么就会将这个块添加到neededReconstruction中。后面会有另外一个独立线程RedundancyMonitor对应生成Reconstruction Task。

对于isPlacementPolicySatisfied()方法,本文不再详细讲解,有兴趣的读者自行阅读代码。

RedundancyMonitor生成和调度ReplicationWork

在我的另外一篇文章 HDFS 块重构和RedundancyMonitor详解 中详细介绍了RedundancyMonitor对neededReconstruction的处理过程,即,将neededReconstruction中的重构任务进行基于优先级的派发,以及DataNode对BlockReconstructionWork的任务的处理过程。但是请注意:

  1. 由节点的decommission或者maintenance所带来的重构任务放在neededReconstruction中。但是,由于decommission/maintenance而带来的low-redundancy block,只是neededReconstruction中Block的来源的一部分。顾名思义,RedundancyMonitor负责的是基于各种原因产生的Low-Redundancy Block的调度。这些原因可能包括(在我的文章HDFS 块重构和RedundancyMonitor详解 中有详细讲解):
    • 本文所讲的,由于节点的Decommission或者Maintenance的发生,DatanodeAdminDefaultMonitor会针对目标节点进行块扫描,从而发现对应的Low-Redundancy Block
    • 在BlockManager中的Daemon Thread中会不断对BlocksMap进行全扫描,会发现Low-Redundancy Block
    • 客户端在写完文件以后会检查文件的所有Block的Redundancy,从而发现对应的Low-Redundancy Block
    • 来自pendingReconstruction中的Block的转换。
  2. HDFS 块重构和RedundancyMonitor详解 中介绍BlockReconstructionWork的任务在DataNode端的处理的时候,我比较偏向于介绍基于Erasure Coding(纠删码)的处理逻辑,因为Erasure Coding的处理方式涉及到多个data 和 parity的block的管理和重算,比基于Replication的块冗余策略更加复杂。有兴趣的读者读了我的这篇文章,完全可以轻松地再去研究基于Replication的块冗余策略的在DataNode端的重构流程。

增加Decommission过程中每次调度的Block数量

  1. Decommission完成的标志以及节点状态的修改
  2. 参数调优

  <property>
    <name>dfs.namenode.replication.work.multiplier.per.iteration</name>
    <value>10</value>
  </property>

在满足每次最大的需要进行replication的数量的情况下,有些节点会因为没有任何一个source node而抛出异常:

    final DatanodeDescriptor[] srcNodes = chooseSourceDatanodes(block,
        containingNodes, liveReplicaNodes, numReplicas,
        liveBlockIndices, priority);
    short requiredRedundancy = getExpectedLiveRedundancyNum(block,
        numReplicas);
    if(srcNodes == null || srcNodes.length == 0) {
      // block can not be reconstructed from any node
      LOG.debug("Block {} cannot be reconstructed from any node", block);
      NameNode.getNameNodeMetrics().incNumTimesReReplicationNotScheduled();
      return null;
    }

从上面的代码可以看到,BlockManager 会通过chooseSourceDatanodes()方法来选择进行replication的源节点。如果没有选择出任何一个源节点,那么显然这个replication是无法工作的。

代码改进:
如果我们无法为一个block选择出任何一个可用的源节点,那么需要打印对应的原因以方便HDFS管理员进行针对性的调整。

      if (priority != LowRedundancyBlocks.QUEUE_HIGHEST_PRIORITY
          && (!node.isDecommissionInProgress() && !node.isEnteringMaintenance())
          && node.getNumberOfBlocksToBeReplicated() >= maxReplicationStreams) {
        continue; // already reached replication limit
      }
      if (priority != LowRedundancyBlocks.QUEUE_HIGHEST_PRIORITY
          && (!node.isDecommissionInProgress() && !node.isEnteringMaintenance())
          && node.getNumberOfBlocksToBeReplicated() >= maxReplicationStreams) {
        continue; // 这里是否可以有TRACE日志,打印无法选择该节点的原因?
      }

      // for EC here need to make sure the numReplicas replicates state correct
      // because in the scheduleReconstruction it need the numReplicas to check
      // whether need to reconstruct the ec internal block
      byte blockIndex = -1;
      if (isStriped) {
        blockIndex = ((BlockInfoStriped) block)
            .getStorageBlockIndex(storage);
        countLiveAndDecommissioningReplicas(numReplicas, state,
            liveBitSet, decommissioningBitSet, blockIndex);
      }

      if (node.getNumberOfBlocksToBeReplicated() >= replicationStreamsHardLimit) {
        continue; // 这里是否可以有TRACE日志,打印无法选择该节点的原因?
      }

同时,我们可以看到,即使一个Block没有为它选出一个合适的源节点而放弃为它构建ReplicationWork,这个节点依然被计数到blocksToProcess中。所以,如果即使我们在一轮调度中设置了一个合理的blocksToProcess值,但是实际上这一轮的大部分调度名额都被那些没有选出合适source节点的Block占用了,实际进行的有效调度进展非常缓慢。
所以,我们最好修改代码,让blocksToProcess指的是有效ReplicationWork的调度的数量,而不是处理的blocks的数量。

代码改进:
在还有如果并没有为当前的Block选出一个合适的源节点,即并没有为这个Block调度对应的ReplicationWork,那么就递进往后选择Block,保证假如priorityQueues中有足够多的需要进行Replicate的Block的情况下,这些可以被调度的Block在一轮调度中能够被调度出去。

怎么避免低优先级的Block一直被饿死?
这也是我们在decommission一个节点的时候遇到的问题。节点decommission导致的ReplicationWork其优先级是很低的,而用户在写入数据所带来的调度的优先级是很高的。因此,假如系统很繁忙,这个decommission会一直没有任何进展。

日志分析和代码印证

2024-07-02 05:27:42,672 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073743420_2596
2024-07-02 05:27:47,482 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Removing pending reconstruction for blk_1073743420_2596
2024-07-02 05:27:47,482 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073743420_2596 on 10.30.0.172:9866 size 2990310 replicaState = FINALIZED
2024-07-02 05:27:47,482 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073743420_2596 on 10.30.0.172:9866 size 2990310 replicaState = FINALIZED
2024-07-02 05:27:47,855 TRACE org.apache.hadoop.hdfs.server.blockmanagement.DatanodeAdminManager: Block blk_1073743420_2596 does not need replication.
2024-07-02 05:27:47,856 TRACE org.apache.hadoop.hdfs.server.blockmanagement.DatanodeAdminManager: Block blk_1073743420_2596 does not need replication.
2024-07-02 05:27:47,856 TRACE org.apache.hadoop.hdfs.server.blockmanagement.DatanodeAdminManager: Block blk_1073743420_2596 does not need replication.
2024-07-02 05:28:17,865 TRACE org.apache.hadoop.hdfs.server.blockmanagement.DatanodeAdminManager: Block blk_1073743420_2596 does not need replication.



2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795480_54656
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795483_54659
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795486_54662
2024-07-02 06:18:49,908 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795487_54663
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795538_54714
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795547_54723
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795548_54724
2024-07-02 06:18:52,910 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795549_54725
2024-07-02 06:18:55,911 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795604_54780
2024-07-02 06:18:55,911 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795605_54781
2024-07-02 06:18:55,911 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795608_54784
2024-07-02 06:18:55,911 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795613_54789
2024-07-02 06:18:58,912 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795651_54827
2024-07-02 06:18:58,912 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795654_54830
2024-07-02 06:18:58,912 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795655_54831
2024-07-02 06:18:58,912 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073795664_54840

2024-07-02 03:20:54,311 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Block blk_1073810374_69550 cannot be reconstructed from any node
2024-07-02 04:56:11,852 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Creating a ReplicationWork to reconstruct blk_1073810374_69550
2024-07-02 04:56:13,920 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Removing pending reconstruction for blk_1073810374_69550
2024-07-02 04:56:13,920 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073810374_69550 on 10.30.0.172:9866 size 9158636 replicaState = FINALIZED
2024-07-02 04:56:13,920 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073810374_69550 on 10.30.0.172:9866 size 9158636 replicaState = FINALIZED
2024-07-02 06:14:38,431 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073810374_69550 on 10.30.0.172:9866 size 9158636 replicaState = FINALIZED
2024-07-02 06:14:38,431 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockManager: Reported block blk_1073810374_69550 on 10.30.0.172:9866 size 9158636 replicaState = FINALIZED

在HDFS的节点选择过程中,如果单个节点的负载过高(xCeiver数量很高),那么这个节点就无法被选作Replica的DataNode。显然, 这种设计适合于DataNode节点数量大于节点的副本数量的情况。但是假如我们是一个很小的HDFS集群,DataNode节点数量就等于默认副本数量,在这种情况下,显然节点负载不应该是考虑因素,我们就应该无条件将一个文件的所有replica分配到所有节点,而不是因为负载过高导致写入文件写入失败,即,这种情况下,这种设计变成了过度设计,将问题恶化。

避免单个节点负载过高,导致这个节点被排除在待分配节点中。下面的DEBUG日志显示了我们的一个节点由于负载过高而被排除:

2024-07-02 08:41:40,343 DEBUG org.apache.hadoop.hdfs.server.blockmanagement.BlockPlacementPolicy: [
Node /rccp205-3a/10.30.0.172:9866 [
  Datanode 10.30.0.172:9866 is not chosen since the node is too busy (load: 11 > 10.0).
  Datanode None is not chosen since required storage types are unavailable  for storage type DISK.
  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值