1. 背景
众所周知,Hadoop2.6.0版本bug非常多,依次大部分公司都将Hadoop升级为Hadoop3。但是,有些实时flink集群由于作业复杂敏感难以迁移,依然使用Hadoop2.6.0集群作为checkpoint存储路径。
如下所示,实时集群中经常出现Non DFS Used非常高,导致可用磁盘空间非常低的情况:
登入机器中,可以发现磁盘只用了20GB:
因此,可以认为这是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 上下文中的副本常见会处于以下两种状态:
- FINALIZED:当副本处于此状态时,对副本的写入已完成,并且副本中的数据被“冻结”(长度已确定),除非重新打开副本以进行追加。具有相同世代标记的块的所有最终副本(称为 GS,定义如下)应该具有相同的数据。最终副本的GS可能会因恢复而增加。
- 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:
对比实验:
将释放rbwSize的代码注释掉:
由于rbwSize一致没有释放,单元测试超时失败:
5.2 DN挂掉测试
当pipeline中的DN挂掉时,其他DN此时rbwSize正常释放至0:
对比实验:
将释放rbwSize的代码注释掉:
由于rbwSize一致没有释放,单元测试超时失败:
5.3 总结
通过上述两个单元测试,结果均符合预期。
6. 灰度测试
通过打log的方式观察patch的生效情况,发现上述patch生效,rbw有效降低:
分别在1天、3天、15天review效果,rbw大小始终小于1GB,patch生效: