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 ------------------------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值