前言
我们知道在HDFS里面有,存着一类白名单和黑名单的列表来控制其下允许进行注册的DN节点。这样可以防止一些外部恶意节点注册到我们的NN上来。在HDFS的概念里,这个黑白名单叫做include file和exclude file。在一般情况下,exclude file的使用范围会更管一些,因为DN的decommission下线需要将待下线机器加到此exclude file中,然后再手动执行dfsadmin的refreshNodes命令进行刷新即可。至于include file白名单,它的管理其实比较复杂。在默认情况下,include file是为空的,意味着默认所有的注册的上来的节点都是被允许的。但是这样会有严重的安全隐患,所以在注重Security环境的集群内,需要管理员每次进行include file的更新来确保当前服务的DN都是有效的。Include file/execlude file的更新需要触发NN的refreshNodes操作来生效。因此这里会存在大量refreshNodes操作的发生。最近笔者在生产环境中就遇到了NN refreshNodes的一些性能问题,本文将讲述围绕refreshNodes的性能问题以及其改进点设计实现。
NN refreshNodes的可用性以及效率问题
笔者在最近工作中遇到了由于refreshNodes失败导致的集群出现大量missing block的情况,鉴于大量missing block造成了比较大的impact。笔者于是开始对此问题进行了深入的分析。
我们从结果开始往前分析,missing block的出现是因为NN上掉了大量节点。NN掉了大量节点的时间段,发现某一台挂掉的DN log里显示了很多下面的Disallow异常:
2021-05-23 07:10:07,609 WARN org.apache.hadoop.hdfs.server.datanode.DataNode: RemoteException in offerService
org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.server.protocol.DisallowedDatanodeException): Datanode denied communication with namenode because the host is not in the include-list: xx.xx.xx.xx:50010
at org.apache.hadoop.hdfs.server.blockmanagement.DatanodeManager.handleHeartbeat(DatanodeManager.java:1499)
at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.handleHeartbeat(FSNamesystem.java:4978)
at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.sendHeartbeat(NameNodeRpcServer.java:1539)
at org.apache.hadoop.hdfs.protocolPB.DatanodeProtocolServerSideTranslatorPB.sendHeartbeat(DatanodeProtocolServerSideTranslatorPB.java:118)
at org.apache.hadoop.hdfs.protocol.proto.DatanodeProtocolProtos$DatanodeProtocolService$2.callBlockingMethod(DatanodeProtocolProtos.java:31228)
看到这个的第一反应,是这个节点不在NN的inlcude host列表里面了,然后才发生的上述拒绝错误。再往上进行分析,又发现了很多诸如“fail to resolve”这类的错误,基本断定是NN解析include host时的dns解析问题。
2021-05-23 07:09:59,030 INFO org.apache.hadoop.util.HostsFileReader: Adding a node "xx.xx.xx.xx" to the list of included hosts from /xxx/hadoop/hosts
2021-05-23 07:09:59,932 WARN org.apache.hadoop.hdfs.server.blockmanagement.HostFileManager: Failed to resolve address `xx.xx.xx.xx` in `/xxx/hadoop/hosts`. Ignoring in the included list.
而触发上面resolve行为的是dfsadmin refreshNodes命令,不过这个refreshNodes执行的耗时相当长。
2021-05-23 07:01:39,001 WARN org.apache.hadoop.ipc.Server: Slow RPC : refreshNodes took 5269MILLISECONDS to process from client Call#0 Retry#0 org.apache.hadoop.hdfs.protocol.ClientProtocol.refreshNodes from xx.xx.xx.xx
后来经过分析发现,refershNodes失败的原因是短时段内大量的dns查询导致了dns解析的延迟,这个延时在NN的代码调用层面来看到的情况,就是解析失败的情况。
在这个问题里,有很多可以值得讨论的点:
-
第一,为什么这里会有这么大量的dns查询请求?后来发现是因为集群DN数总量在持续不断变多,导致每次refreshNodes的cost越来越高。因为NN的refreshNodes行为会重刷所有的DN节点,相当于这是一个全量解析host地址的行为。
-
第二,为什么这里没有NN解析重试行为?refreshNodes执行失败了,最好能够有一个重试机制,否则失败造成的掉节点,missing block对用户影响太大。
-
第三,现有refreshNodes的效率不高,它是一种全量解析的方式,是否我们能够支持增量式地refresh node,对于大部分分状态不变的节点,NN完全没必要再对其做dns的重新解析。在增量式 refesh node的情况里,admin管理员能够指定特定的host进行node的refresh,从而使得refreshNode的操作变得非常轻量级。
针对上面的第二,第三点,我们内部对refreshNode逻辑进行了调整和改进,使其在可用性以及执行效率上得到一个号的提升。
NN refreshNodes的改造升级
针对上节部分提出的几个关键点,我们对此进行了如下两部分的改进。
refreshNodes重试机制的引入
在原有逻辑里,NN refreshNodes会触发重新读取所有的include/exclude文件中的host信息。在这个过程里,这些文件里可能会包含有上千甚至上万个节点的host信息,每次读取到一个新的host后,NN尝试会进行对其host的ip地址的解析,相关代码如下:
private static HostSet readFile(String type, String filename)
throws IOException {
HostSet res = new HostSet();
if (!filename.isEmpty()) {
HashSet<String> entrySet = new HashSet<String>();
HostsFileReader.readFileToSet(type, filename, entrySet);
for (String str : entrySet) {
// 构造InetSocketAddress地址时,会进行地址解析
InetSocketAddress addr = parseEntry(type, filename, str);
if (addr != null) {
res.add(addr);
}
}
}
return res;
}
@VisibleForTesting
static InetSocketAddress parseEntry(String type, String fn, String line) {
try {
URI uri = new URI("dummy", line, null, null, null);
int port = uri.getPort() == -1 ? 0 : uri.getPort();
InetSocketAddress addr = new InetSocketAddress(uri.getHost(), port);
if (addr.isUnresolved()) {
LOG.warn(String.format("Failed to resolve address `%s` in `%s`. " +
"Ignoring in the %s list.", line, fn, type));
return null;
}
return addr;
} catch (URISyntaxException e) {
LOG.warn(String.format("Failed to parse `%s` in `%s`. " + "Ignoring in " +
"the %s list.", line, fn, type));
}
return null