我们知道,在NameNode节点启动的时候,会启动一些后台的任务线程,例如:
关于这些个后台监控线程有神马具体的用途,我在这里不会一一作出解释,但我会在以后的博文中来具体阐述,因为他们基本上担负起了整个NameNode节点的管理工作。在本文,我将具体的讲述DecommissionManager$Monitor这个后台工作线程。
先来看看DecommissionManager$Monitor是用来干神马的吧!DecommissionManager主要是负责管理节点退役或者说节点停用(注意:这里的节点指的是DataNode节点),因为我们不能说让一个DataNode节点退役,就能马上让它退役的,而是要等到该DataNode节点上的所有数据块Blocks被复制完成之后,才能允许该DataNode节点退役。这就需要Monitor负责定时地检测这些节点中Blocks的状态,当这些Blocks都满足副本因子之后,才能将该DataNode节点置为退役状态,因此我们可以看出,在DecommissionManager里其实也只有Monitor在真正执行功能。下面就来看看Monitor的检测实现吧!
关于影响Monitor检测性能的参数:
class Monitor implements Runnable {
private final long recheckInterval; //一次轮询检查的时间间隔
private final int numNodesPerCheck; //每次轮询检查处于正在退役状态的数据节点数量
private String firstkey = ""; //上一次轮询结束时处理到的最后一个数据节点
}
因为需要检测全部的DataNode,然后DataNode又比较多,所以采用迭代的步长检测,例如每次只检测处于正在退役状态的numNodesPerCheck个数据节点,下次检测时就从上次检测的的最后一个数据节点开始,而这两次检测的时间间隔是recheckInterval s,又为了保证检测的连续性,所以检测的数据构成一个环形数据结构,然后需要记录每次迭代的起始点,所以有个firstkey属性,然后环形数据结构由hadoop自己实现的CyclicIteration来提供(其实就是首尾数据相连)。
class Monitor implements Runnable {
...
/**
* 不停地轮询检查
*/
public void run() {
for(; fsnamesystem.isRunning(); ) {
synchronized(fsnamesystem) {
check();
}
try {
Thread.sleep(recheckInterval);
} catch (InterruptedException ie) {
LOG.info("Interrupted " + this.getClass().getSimpleName(), ie);
}
}
}
/**
* 检查一批处于正在退役状态的数据节点是否可以退役
*/
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();
if(d.isDecommissionInProgress()) { //数据节点处于正在退役状态
try {
fsnamesystem.checkDecommissionStateInternal(d); //检查数据节点是否可以退役,如果可以则使其退役
} catch(Exception e) {
LOG.warn("entry=" + entry, e);
}
//检查处于正在退役状态的数据节点达到设定的阈值就退出本次轮询
if(++count == numNodesPerCheck) {
return;
}
}
}
}
}
这样设计的目的主要是判断一个正在退役状态的数据节点是否可以退役,需要检查存储在它上面的所有数据块Block的副本是否已经满足用户设置的副本值,如果不满足还要讲该数据块Block添加到待复制副本的队列中,这个检查过程是比较耗时的,其详细过程如下:
/**
* 检查一个数据节点上的每一个数据块的副本数量是否满足设置的值,如果都满足则返回false,否则返回ture;
* 对于副本数量不满足设置值的数据块,如果还没有对该数据块进行副本复制,则将其挂载到副本复制线程的任务队列中
*/
private boolean isReplicationInProgress(DatanodeDescriptor srcNode) {
boolean status = false;
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);
if(curExpectedReplicas > curReplicas) {
status = true;
//数据块不在待(副本)复制队列中,也不在正在(副本)复制队列中
if(!neededReplications.contains(block) && pendingReplications.getNumReplicas(block) == 0) {
neededReplications.add(block, curReplicas, num.decommissionedReplicas(), curExpectedReplicas);
}
}
}//if
}//for
return status;
}
/**
* 检查一个数据节点是否已退役或者能否退役,若能够退役,则使其退役
*/
boolean checkDecommissionStateInternal(DatanodeDescriptor node) {
if (node.isDecommissionInProgress()) {
if (!isReplicationInProgress(node)) { //存储在该数据节点的所有数据块的副本数量都满足其设置值,则使其退役
node.setDecommissioned();
LOG.info("Decommission complete for node " + node.getName());
}
}
if (node.isDecommissioned()) {
return true;
}
return false;
}
顺带提一下,recheckInterval和numNodesPerCheck是通过配置文件来设置的,它们对应的配置项分别为:
dfs.namenode.decommission.interval
dfs.namenode.decommission.nodes.per.interval
这里重复一下Monitor检测处理逻辑:
1:判断节点是否处于DECOMMISSION_INPROGRESS状态;
2:如果处于DECOMMISSION_INPROGRESS状态,则检测该节点上的所有数据块是否已经到了复制因子;
3:如果已经到了复制因子块的个数则说明这个节点上的所有块都是多余了,也就是说这个节点可以退役了,将节点状态设置为退役。
从上面的检测逻辑可以看出,其实就是为了确定这个节点上的所有数据块经过复制是否已经达到了其设置的副本数量(除该副本之外),如果完全复制过了,那这个节点其实就没用了,所以就标示为退役了。之所以要这样做,是因为如果立马让这个DataNode节点shutdown,那么有可能在某些异常下发生数据块丢失的严重错误。例如:待退役的数据节点dn1上有一个数据块b1,如果立马让dn1退役,那么数据块b1就需要其它的数据节点来复制一个副本,如果b1其它的副本所在的数据节点都宕机了,那么数据块b1就丢失了。在这里,Monitor仅仅只是将数据节点标记为已退役状态,而关于数据节点退役后的清理工作最后是由HeartbeatMonitor后台线程来处理的。退役检测处理的过程中主要涉及到两个状态变量:
AdminStates.DECOMMISSION_INPROGRESS;
AdminStates.DECOMMISSIONED;
前一个变量表示DataNode节点处于退役处理状态、后一个变量表示节点处于已退役状态。当然,在正常的情况下,DataNode节点一般由AdminStates.DECOMMISSION_INPROGRESS变为AdminStates.DECOMMISSIONED状态,那么这又是由谁触发的呢?
1. Include/Exclude Host列表的刷新重载
NameNode为客户端提供了一个方法refreshNodes(),当客户端调用该接口的时候,NameNode节点会重新加载配置文件中的dfs.hosts和dfs.hosts.exclude两个选项,更新HostsFileReader,之后根据HostsFileReader的新主机列表来判断哪些DataNode节点被允许连接到NameNode,哪些不允许被连接,对于不允许连接的DataNode节点,先将其标记为AdminStates.DECOMMISSION_INPROGRESS,之后就开始对该DataNode节点上的数据块Blocks进行复制,使之满足副本因子(Blocks复制是由其它工作线程监督完成的,而只需要将需要replication的Blocks放到一个按优先级的队列中即可),那么接下来就是DecommissionManager$Monitor线程要干的事情了。
2. 数据节点注册
当数据节点重启或者重新初始化之后,都会首先向NameNode节点注册,如果此时主节点不允许该数据节点加入集群,那么它就会对该数据节点进行强制退役处理。
当DecommissionManager$Monitor线程把一个DataNode节点标记为AdminStates.DECOMMISSIONED状态之后,NameNode节点会在当该DataNode节点下一次发送heart的时候,就明确的给他一个command,让它shutdown。