HDFS中Non DFS Used容量异常排查实践
  GQ7psP7UJw7k 2023年11月19日 18 0

1. 背景

众所周知,Hadoop2.6.0版本bug非常多,依次大部分公司都将Hadoop升级为Hadoop3。但是,有些实时flink集群由于作业复杂敏感难以迁移,依然使用Hadoop2.6.0集群作为checkpoint存储路径。

如下所示,实时集群中经常出现Non DFS Used非常高,导致可用磁盘空间非常低的情况:

Untitled.png

登入机器中,可以发现磁盘只用了20GB:

Untitled 1.png

因此,可以认为这是HDFS的一个BUG。

2. Non DFS Used计算逻辑

datanode向namenode上报心跳时,调用FsDatasetImpl#getStorageReports方法:

  public StorageReport[] getStorageReports(String bpid)
      throws IOException {
    List<StorageReport> reports;
    synchronized (statsLock) {
      List<FsVolumeImpl> curVolumes = volumes.getVolumes();
      reports = new ArrayList<>(curVolumes.size());
      for (FsVolumeImpl volume : curVolumes) {
        try (FsVolumeReference ref = volume.obtainReference()) {
          StorageReport sr = new StorageReport(volume.toDatanodeStorage(),
              false,
              volume.getCapacity(),
              volume.getDfsUsed(),
              volume.getAvailable(),
              volume.getBlockPoolUsed(bpid));
          reports.add(sr);
        } catch (ClosedChannelException e) {
          continue;
        }
      }
    }

    return reports.toArray(new StorageReport[reports.size()]);
  }

其中可用空间的计算逻辑如下:

  public long getAvailable() throws IOException {
    long remaining = getCapacity() - getDfsUsed() - reservedForRbw.get();
    # 计算可用信息
    long available = usage.getAvailable() - reserved - reservedForRbw.get();
    if (remaining > available) {
      remaining = available;
    }
    return (remaining > 0) ? remaining : 0;
  }

其中,usage.getAvailable通过linux获取DataNode磁盘的可用空间:

    public long getUsableSpace() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
            sm.checkRead(path);
        }
        if (isInvalid()) {
            return 0L;
        }
        return fs.getSpace(this, FileSystem.SPACE_USABLE);
    }

而reserved就是设置的预留空间,为50GB:

  public static final String  DFS_DATANODE_DU_RESERVED_KEY = "dfs.datanode.du.reserved";
  public static final long    DFS_DATANODE_DU_RESERVED_DEFAULT = 0;
  this.reserved = conf.getLong(
        DFSConfigKeys.DFS_DATANODE_DU_RESERVED_KEY,
        DFSConfigKeys.DFS_DATANODE_DU_RESERVED_DEFAULT);

Non DFS Used不是一个原生指标,它是通过capacity - dfsUsed - remaining计算出来。当Hadoop中可用空间越多,那么NonDfsUsed就越低;反之越高:

public long getNonDfsUsed() {
  long nonDFSUsed = capacity - dfsUsed - remaining;
  return nonDFSUsed < 0 ? 0 : nonDFSUsed;
}

3. 问题思考

根据Non DFS Used指标公式可以想到:Hadoop磁盘可用空间异常变小,并不是Non DFS Used引起,而是因为remaining=getCapacity() - getDfsUsed() - reservedForRbw.get()这个公式中,有一环出现问题。

getCapacity()直接获取的是linux磁盘大小。

getDfsUsed()直接获取datanode内存中已经使用的空间,这部分不可能出错,不然hdfs稳定性太差,也太不可靠了。。。

最后,就剩reservedForRbw.get() 这个值可能出错了。当dn进程启动时,reservedForRbw大小为0:

    this.reservedForRbw = new AtomicLong(0L);

如果reservedForRbw越来越大,那么available必然越来越小。

4. 问题排查

DataNode 上下文中的副本常见会处于以下两种状态:

  1. FINALIZED:当副本处于此状态时,对副本的写入已完成,并且副本中的数据被“冻结”(长度已确定),除非重新打开副本以进行追加。具有相同世代标记的块的所有最终副本(称为 GS,定义如下)应该具有相同的数据。最终副本的GS可能会因恢复而增加。
  2. RBW(正在写入的副本):这是正在写入的任何副本的状态,无论文件是为写入而创建的,还是为追加而重新打开的。RBW 副本始终是打开文件的最后一个块。数据仍在写入副本,尚未最终确定。RBW 副本的数据(不一定是全部)对读取器客户端可见。如果发生任何故障,将尝试将数据保留在 RBW 副本中。

在DataNode中,申请写入块时会申请对应空间大小的reservedForRbw,当块状态为FINALIZED时,申请的空间大小会在reservedForRbw中进行回收。通过扫荡行排查,发现社区针对rbw空间的释放,有三个相关patch:

  • BlockReceiver在构建流水线时,一旦发生IOException异常,就应该释放rbw空间

https://issues.apache.org/jira/browse/HDFS-6955

diff --git hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java
index bc5396f..957b2c7 100644
--- hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java
+++ hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java
@@ -117,7 +117,7 @@
   /** the block to receive */
   private final ExtendedBlock block;
   /** the replica to write */
-  private final ReplicaInPipelineInterface replicaInfo;
+  private ReplicaInPipelineInterface replicaInfo;
   /** pipeline stage */
   private final BlockConstructionStage stage;
   private final boolean isTransfer;
@@ -259,6 +259,9 @@
     } catch (ReplicaNotFoundException bne) {
       throw bne;
     } catch(IOException ioe) {
       //当IOException时,释放rbw
+      if (replicaInfo != null) {
+        replicaInfo.releaseAllBytesReserved();
+      }
       IOUtils.closeStream(this);
       cleanupBlock();
  • 在构建BlockReceiver对象后,在调用receiveBlock前,即writeBlock时出现异常时,namenode会发出清理该block命令,此时应该释放rbw:

现状:

# 在构建BlockReceiver对象时,如果异常,会被catch,同时会清理rbw。
# 在调用receiveBlock方法时,如果异常,会被catch,同时会清理rbw。
# 如果在两者过程中,即writeBlock时,发生异常,rbw无法释放。

patch:

https://issues.apache.org/jira/browse/HDFS-9530

--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java
@@ -81,6 +81,7 @@
 import org.apache.hadoop.hdfs.server.datanode.ReplicaBeingWritten;
 import org.apache.hadoop.hdfs.server.datanode.ReplicaHandler;
 import org.apache.hadoop.hdfs.server.datanode.ReplicaInPipeline;
+import org.apache.hadoop.hdfs.server.datanode.ReplicaInPipelineInterface;
 import org.apache.hadoop.hdfs.server.datanode.ReplicaInfo;
 import org.apache.hadoop.hdfs.server.datanode.ReplicaNotFoundException;
 import org.apache.hadoop.hdfs.server.datanode.ReplicaUnderRecovery;
                      # invalidate方法就是清理block
@@ -1848,6 +1849,9 @@ public void invalidate(String bpid, Block invalidBlks[]) throws IOException {
           LOG.debug("Block file " + removing.getBlockFile().getName()
               + " is to be deleted");
         }
         //此时释放rbw空间
+        if (removing instanceof ReplicaInPipelineInterface) {
+          ((ReplicaInPipelineInterface) removing).releaseAllBytesReserved();
+        }
       }

备注:

  • pipeline中的单个DN挂掉后,其他DN此时已经写入了一部分block数据到磁盘,在每台DN检查该block状态时,应该释放rbw:

https://issues.apache.org/jira/browse/HDFS-11674

diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java
index ec86337b1f1..c13b6f51286 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java
                       //准备恢复block
@@ -2359,6 +2359,10 @@ static ReplicaRecoveryInfo initReplicaRecovery(String bpid, ReplicaMap map,
       //检查block状态,如果block状态是rbw,释放这部分空间。
       //后续客户端会在这部分block基础上,继续写文件
       LOG.info("initReplicaRecovery: changing replica state for "
           + block + " from " + replica.getState()
           + " to " + rur.getState());
+      if (replica.getState() == ReplicaState.TEMPORARY || replica
+          .getState() == ReplicaState.RBW) {
+        ((ReplicaInPipeline) replica).releaseAllBytesReserved();
+      }
     }
     return rur.createInfo();

5. 单元测试

5.1 writeBlock异常测试

writeBlock异常时,最终rbwSize正常释放至0:

Untitled 2.png

对比实验:

将释放rbwSize的代码注释掉:

Untitled 3.png

由于rbwSize一致没有释放,单元测试超时失败:

Untitled 4.png

5.2 DN挂掉测试

当pipeline中的DN挂掉时,其他DN此时rbwSize正常释放至0:

Untitled 5.png 对比实验:

将释放rbwSize的代码注释掉:

Untitled 6.png

由于rbwSize一致没有释放,单元测试超时失败:

Untitled 7.png

5.3 总结

通过上述两个单元测试,结果均符合预期。

6. 灰度测试

通过打log的方式观察patch的生效情况,发现上述patch生效,rbw有效降低:

Untitled 8.png

分别在1天、3天、15天review效果,rbw大小始终小于1GB,patch生效:

Untitled 9.png

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月19日 0

暂无评论

推荐阅读
GQ7psP7UJw7k