在Hadoop集群中,按照集群规模来划分,规模可大可小,大的例如百度,据说有4000台规模大小的Hadoop集群,小的话,几十台机器组成的集群也都是存在的。但是不论说是大型的集群以及小规模的集群,都免不了出现节点故障的情况,尤其是超大型的集群,节点故障几乎天天发生,因此如何做到正确,稳妥的故障情况处理,就显得很重要了,这里提供一个在Hadoop集群中可以想到的办法,就是Decommission操作,节点下线操作,一般的情况是故障节点已经是一个dead节点,或是出现异常情况的节点。此时如若不处理,或许会影响到整个集群的性能。所以在这里分享一下Hadoop中的Decommision机制。
在分析相关源码之前,有必要了解一下,让一个数据节点下线的物理操作,操作步骤其实很简单,在以前老版本的Hadoop中,好像是可以通过hadoop dfsadmin带参数的形式执行,但是在最近新版的Hadoop中好像这类命令失效了,于是我在做测试的时候,用了一个更通用的办法来触发这一行为,就是把目标下线节点加入execlude文件中,就是拒绝接入Hadoop集群的节点名单,姑且可以理解为黑名单列表,对应的是include名单,默认这2个名单都没配,所以数据节点一启动,就会注册到namenode节点上。然后是再执行Hadoop 的refreshnode命令,此命令就会从对应的此配置文件中读取最新的数据节点信息,然后开始decommission操作,在50070的ui界面上就可以看到待下线节点的状态会从active状态变为decommision in progress,此时此数据节点的block将会被逐步的拷贝出去,最后随着操作的完成,最终状态就会被变为decommissioned,此时就可以正式下线此节点,用hadoop-demons.sh namenode stop即可。在执行过程的前后,可以执行hadoop fsck的方法观察block块的路径,判断block拷贝情况。
为什么花了这么多的篇幅介绍,decommision操作呢,因为操作的顺序与实际代码的运行流程基本吻合,有很强的关联性。在下面具体的分析过程中将会逐步体现出来。下面简要列出相关的2个类。
1.DecommissionManager--decommission操作管理类,里面包含了decommission操作状态监控。
2.FSNamesystem--这是一个大的操作类,内部包含了许多模块的工作,包括之前介绍过的副本相关操作也是部分在此类中进行中转,与decommission主要的方法refreshNode()方法包含于此。
在物理操作中,decommision操作的触发是因为添加了execlude文件,然后再输入refreshNode命令开始的,与此就会对应到了FSNamesystem的同名方法
/** * Rereads the config to get hosts and exclude list file names. * Rereads the files to update the hosts and exclude lists. It * checks if any of the hosts have changed states: * 1. Added to hosts --> no further work needed here. * 2. Removed from hosts --> mark AdminState as decommissioned. * 3. Added to exclude --> start decommission. * 4. Removed from exclude --> stop decommission. * 重新从配置中读取节点列表,移除掉准备下线的列表等 */ public void refreshNodes(Configuration conf) throws IOException { checkSuperuserPrivilege(); // Reread the config to get dfs.hosts and dfs.hosts.exclude filenames. // Update the file names and refresh internal includes and excludes list if (conf == null) conf = new Configuration(); //重新读取配置文件中的dfs.hosts以及dfs.hosts.exclude属性 hostsReader.updateFileNames(conf.get("dfs.hosts",""), conf.get("dfs.hosts.exclude", "")); hostsReader.refresh(); ....果然在这里会重新去读exclude,include文件数据,然后这里会遍历当前的数据节点,与配置中新增的节点进行匹配
synchronized (this) { //遍历数据节点 for (Iterator<DatanodeDescriptor> it = datanodeMap.values().iterator(); it.hasNext();) { DatanodeDescriptor node = it.next(); // Check if not include. //判断数据节点是否在允许的主机列表内 if (!inHostsList(node, null)) { //如果不是,则把此节点的状态设为Decommissioned,代表着此节点准备下线 node.setDecommissioned(); // case 2. } else {inHost系列的判断方法如下,inExclude与此方法同,不附上代码
/** * Keeps track of which datanodes/ipaddress are allowed to connect to the namenode. * 如何判断节点是否包含在允许接入列表中的判断方法,exclude列表是同样的道理 */ private boolean inHostsList(DatanodeID node, String ipAddr) { //从hostReader中读取最新的host列表 Set<String> hostsList = hostsReader.getHosts(); //利用主机名去判断 return (hostsList.isEmpty() || (ipAddr != null && hostsList.contains(ipAddr)) || hostsList.contains(node.getHost()) || hostsList.contains(node.getName()) || ((node instanceof DatanodeInfo) && hostsList.contains(((DatanodeInfo)node).getHostName()))); }判断完毕之后,会进行逻辑判断,如果节点在exclude名单中,代表准备下线,则修改其状态,如果是正在下线的节点,则无须操作。完整逻辑如下
/** * Rereads the config to get hosts and exclude list file names. * Rereads the files to update the hosts and exclude lists. It * checks if any of the hosts have changed states: * 1. Added to hosts --> no further work needed here. * 2. Removed from hosts --> mark AdminState as decommissioned. * 3. Added to exclude --> start decommission. * 4. Removed from exclude --> stop decommission. * 重新从配置中读取节点列表,移除掉准备下线的列表等 */ public void refreshNodes(Configuration conf) throws IOException { checkSuperuserPrivilege(); // Reread the config to get dfs.hosts and dfs.hosts.exclude filenames. // Update the file names and refresh internal includes and excludes list if (conf == null) conf = new Configuration(); //重新读取配置文件中的dfs.hosts以及dfs.hosts.exclude属性 hostsReader.updateFileNames(conf.get("dfs.hosts",""), conf.get("dfs.hosts.exclude", "")); hostsReader.refresh(); synchronized (this) { //遍历数据节点 for (Iterator<DatanodeDescriptor> it = datanodeMap.values().iterator(); it.hasNext();) { DatanodeDescriptor node = it.next(); // Check if not include. //判断数据节点是否在允许的主机列表内 if (!inHostsList(node, null)) { //如果不是,则把此节点的状态设为Decommissioned,代表着此节点准备下线 node.setDecommissioned(); // case 2. } else { //入如果此节点是包含在不允许接入的列表名单中时 if (inExcludedHostsList(node, null)) { //判断此时状态是否为还没开始下线操作,如果是开始decommission if (!node.isDecommissionInProgress() && !node.isDecommissioned()) { startDecommission(node); // case 3. } } else { //如果是其他的情况,如果节点处于decommsion操作,则停止操作 if (node.isDecommissionInProgress() || node.isDecommissioned()) { stopDecommission(node); // case 4. } } } } } }然后开始沿着decommision操作继续往里走,就是startDecommision方法
/** * Start decommissioning the specified datanode. * 对指定节点开始进行decommission操作 */ private void startDecommission (DatanodeDescriptor node) throws IOException { if (!node.isDecommissionInProgress() && !node.isDecommissioned()) { LOG.info("Start Decommissioning node " + node.getName()); node.startDecommission(); //设置节点decommison开始时间 node.decommissioningStatus.setStartTime(now()); // // all the blocks that reside on this node have to be // replicated. //检查此时的decommission操作状态 checkDecommissionStateInternal(node); } }
在这个方法主要操作还是在于让节点启动相应的decommision下线操作,开始一波副本的拷贝工作了,在这里感觉第一次启动,就在这里判断decommision状态,个人感觉没有必要,一般操作不会很快结束,一般的decomission监控会有额外的线程周期性的监控此类操作。而这个线程进行检查的函数也是checkDecommissionStateInternal方法,他是如何进行检查判断的呢
/** * Change, if appropriate, the admin state of a datanode to * decommission completed. Return true if decommission is complete. * decommision的状态检测是根据其上的副本量来衡量的 */ boolean checkDecommissionStateInternal(DatanodeDescriptor node) { // // Check to see if all blocks in this decommissioned // node has reached their target replication factor. // if (node.isDecommissionInProgress()) { //调用副本进度判断函数 if (!isReplicationInProgress(node)) { node.setDecommissioned(); LOG.info("Decommission complete for node " + node.getName()); } } if (node.isDecommissioned()) { return true; } return false; }答案就在上面,通过剩余副本的拷贝情况,如果isReplicationInPogress返回FALSE代表了,已经全部完成拷贝工作了,状态就可以修改为decomissioned结束状态了,下面仔细看看这个isReplicationInProgress方法
/** * Return true if there are any blocks on this node that have not * yet reached their replication factor. Otherwise returns false. * 如果当前数据节点block块的副本系数还没有满足期望的副本数值值,则表明还要添加复制请求 */ private boolean isReplicationInProgress(DatanodeDescriptor srcNode) { boolean status = false; int underReplicatedBlocks = 0; int decommissionOnlyReplicas = 0; int underReplicatedInOpenFiles = 0; //遍历此节点上的所有数据块 for(final Iterator<Block> i = srcNode.getBlockIterator(); i.hasNext(); ) { final Block block = i.next(); INode fileINode = blocksMap.getINode(block); if (fileINode != null) { NumberReplicas num = countNodes(block); //获取此数据块当前的副本数 int curReplicas = num.liveReplicas(); //获取此副本块的期望副本块数 int curExpectedReplicas = getReplication(block); //如果期望副本块数大于当前副本块数,表明block还需要复制 if (curExpectedReplicas > curReplicas) { // Log info about one block for this node which needs replication if (!status) { //做状态的修改,表明block还需要复制 status = true; logBlockReplicationInfo(block, srcNode, num); } underReplicatedBlocks++; if ((curReplicas == 0) && (num.decommissionedReplicas() > 0)) { decommissionOnlyReplicas++; } if (fileINode.isUnderConstruction()) { underReplicatedInOpenFiles++; } if (!neededReplications.contains(block) && pendingReplications.getNumReplicas(block) == 0) { // // These blocks have been reported from the datanode // after the startDecommission method has been executed. These // blocks were in flight when the decommission was started. // //添加新的副本复制请求 neededReplications.add(block, curReplicas, num.decommissionedReplicas(), curExpectedReplicas); } } } } srcNode.decommissioningStatus.set(underReplicatedBlocks, decommissionOnlyReplicas, underReplicatedInOpenFiles); return status; }原理很简单,对于待撤销数据节点上的每个block块,判断当前副本与期望副本数之间的差,如果不满足,就增强复制请求,至此,decommision这个核心流程就走通了.下面看看一个常驻的监控线程,毕竟他才是主要做监控进度这项任务的.
decommisionMannager,decommision操作管理器,所包含的变量很少
/** * Manage node decommissioning. * 节点Decommission操作状态管理器 */ class DecommissionManager { static final Log LOG = LogFactory.getLog(DecommissionManager.class); //名字空间系统 private final FSNamesystem fsnamesystem; DecommissionManager(FSNamesystem namesystem) { this.fsnamesystem = namesystem; } /** Periodically check decommission status. */ //监控方法 class Monitor implements Runnable { ... }
/** Periodically check decommission status. */ //监控方法 class Monitor implements Runnable { /** recheckInterval is how often namenode checks * if a node has finished decommission * 定期检查周期 */ private final long recheckInterval; /** The number of decommission nodes to check for each interval */ private final int numNodesPerCheck; /** firstkey can be initialized to anything. */ private String firstkey = ""; Monitor(int recheckIntervalInSecond, int numNodesPerCheck) { this.recheckInterval = recheckIntervalInSecond * 1000L; this.numNodesPerCheck = numNodesPerCheck; } /** * Check decommission status of numNodesPerCheck nodes * for every recheckInterval milliseconds. */ public void run() { for(; fsnamesystem.isRunning(); ) { synchronized(fsnamesystem) { //调用check()方法 check(); } try { Thread.sleep(recheckInterval); } catch (InterruptedException ie) { LOG.info("Interrupted " + this.getClass().getSimpleName(), ie); } } }for循环内持周期性的续调check()方法,如果系统没有结束的话,docheck方法又会调用之前提到的checkDecommission的方法
private void check() { int count = 0; //遍历每个数据节点 for(Map.Entry<String, DatanodeDescriptor> entry : new CyclicIteration<String, DatanodeDescriptor>( fsnamesystem.datanodeMap, firstkey)) { final DatanodeDescriptor d = entry.getValue(); firstkey = entry.getKey(); //如果数据节点正处于decommison操作的话,则做检查 if (d.isDecommissionInProgress()) { try { //调用fsnamesystem的checkDecommissionStateInternal方法,此方法内部又会调用isReplicationInProgress进行副本的 //情况判断 fsnamesystem.checkDecommissionStateInternal(d); } catch(Exception e) { LOG.warn("entry=" + entry, e); } if (++count == numNodesPerCheck) { return; } } } } }他是每个数据节点遍历着判断.OK,希望通过我的分析,大家对Decommission有更多的了解,有所收获.
全部代码的分析请点击链接https://github.com/linyiqun/hadoop-hdfs,后续将会继续更新HDFS其他方面的代码分析。
《Hadoop技术内部–HDFS结构设计与实现原理》.蔡斌等