前面介绍Networktopology结构的时候就说过,NameNode把注册的DataNode节点按照他们的ip地址存储到一个树状网络拓扑图中(对应Networktopology的一个实例),然后NameNode调用ReplicationTargetChooser来为每一个数据块副本选择合适的存储节点。那么,NameNode是如何把一个数据节点按照它的ip解析到对应的树状网络拓扑图中的一个叶子节点呢?
实际上,这个过程很简单,NameNode把这个NameNode节点所在主机的ip地址按照用户设计的规则转换成一个对应的路径格式(如:/*/*/*/*),这个路径就对应了树状网络拓扑图中的一个非叶子节点,NameNode就把这个数据节点存放到这个非叶子节点下面,作为它的叶子节点。现在关键的问题就是NameNode如何把一个ip地址解析成一个路径的形式。刚才说了,NameNode是按照用户设计的转换规则来的,它自己并不知道如何转换,也就是说这个转换是交由用户自己来实现的。
HDFS将ip地址转换成路径的操作交给了用户自己来实现,即用户通过自己实现接口DNSToSwitchMapping,然后在配置文件中指明实现的这个类,NameNode就能够自动的调用用户的解析实现了。哦,对了,这个对应的配置项是:topology.node.switch.mapping.impl。
现在我想说一下,HDFS为什么要把ip地址的解析交给用户自己来实现?这是因为HDFS不得不把这个过程交给用户来实现,只有集群管理员即用户才知道哪些ip地址对应的机器被放在同一个机架下面,哪些机架在同一个路由器下面,说白了就是只有集群管理员才知道机器之间的网络距离。好了,现在就来具体看看与DNSToSwitchMapping相关的实现。
HDFS考虑到性能问题,一方面把DNSToSwitchMapping接口定义成一个批量解析任务,另一方面自己还定义了一个缓存类CacheDNSToSwitchMapping来保存解析的结果。同时,HDFS自己也定义了一个DNSToSwitchMapping的实现RawScriptBasedMapping,这个实现类被设计成了一个插件集成类,它通过执行shell脚本语言调用第三方的实现来获取一批ip地址的解析结果,这实际上还是将真正的解析过程交给了用户。在默认的情况下,NameNode使用ScriptBasedMapping来解析ip地址。现在来好好的分析一下这个RawScriptBasedMapping实现类。
RawScriptBasedMapping类主要包含三个参数:
scriptName:调用第三方ip解析实现的脚本命令;
maxArgs:第三方ip解析实现一次调用最多能解析多少个ip;
conf:通过配置文件获取scriptName和maxArgs;
RawScriptBasedMapping根据NameNode传过来的ip解析任务分批调用第三方ip解析器来执行这些ip的解析,每一批不能超过maxArgs个ip地址。调用的过程如下:
private String runResolveCommand(List<String> args) {
int loopCount = 0;
if (args.size() == 0) {
return null;
}
StringBuffer allOutput = new StringBuffer();
int numProcessed = 0;
if (maxArgs < MIN_ALLOWABLE_ARGS) {
LOG.warn("Invalid value " + Integer.toString(maxArgs) + " for " + SCRIPT_ARG_COUNT_KEY + "; must be >= " + Integer.toString(MIN_ALLOWABLE_ARGS));
return null;
}
while (numProcessed != args.size()) {
int start = maxArgs * loopCount;
List <String> cmdList = new ArrayList<String>();
cmdList.add(scriptName);
for (numProcessed = start; numProcessed < (start + maxArgs) && numProcessed < args.size(); numProcessed++) {
cmdList.add(args.get(numProcessed));
}
File dir = null;
String userDir;
if ((userDir = System.getProperty("user.dir")) != null) {
dir = new File(userDir);
}
ShellCommandExecutor s = new ShellCommandExecutor(cmdList.toArray(new String[0]), dir);
try {
s.execute();
allOutput.append(s.getOutput() + " ");
} catch (Exception e) {
LOG.warn(StringUtils.stringifyException(e));
return null;
}
loopCount++;
}
return allOutput.toString();
}
现在我将举一个完整的例子,即自己实现一个第三方的ip解析插件,来详细的阐述整个实现与配置过程:
1).自定义一个ip解析器
利用C语言写一个简单的程序,对每一个传入的ip地址,都输出对应的一个路径,然后编译连接生成一个可执行文件:ipresolve
#include<stdio.h>
int main(int argc, char ** argv){
if(argc>1){
int i;
for(i=1; i<argc; ++i) printf("/rack/rakc/rakc%d\n",i);
}
return 0;
}
2).在HDFS的配置文件core-site.xml中配置这个第三方插件
<property>
<name>topology.script.file.name</name>
<value>/home/**/ipresolve</value>
<description>可执行文件ipresolve的绝对路径</description>
</property>
<property>
<name>topology.script.number.args</name>
<value>10</value>
<description></description>
</property>
3).测试
public static void main(String[] args){
Configuration conf = new Configuration();
ScriptBasedMapping sbp = new ScriptBasedMapping();
sbp.setConf(conf);
List<String> ips = new ArrayList<String>();
ips.add("127.0.0.1");
ips.add("127.0.0.2");
ips.add("127.0.0.3");
ips.add("127.0.0.4");
List<String> result = sbp.resolve(ips);
for(int i=0; i<result.size(); ++i){
System.out.println(result.get(i));
}
}
4).测试输出
当然,对用户来说有两种方式来实现用户自定义的ip解析器,一中是直接实现DNSToSwitchMapping接口,并将这个实现类配置到配置文件的项;另一种就是上面的的插件方式。就性能而言,第一种方式为佳,就可扩展行而言,第二种方式为佳,不过,这最后还是取决于用户自己的应用场景。