Namenode维护着HDFS两个最重要的关系,第一关系是文件系统目录树,第二关系便是数据块和数据节点的关系了。
数据块和数据节点的对应关系,就是指定数据块的副本保存在哪些数据节点上的信息。这个信息是在Datanode启动时,由Datanode上报给Namenode的。Namenode收到后再去更新内存中的数据,以维护数据块和数据节点的对应关系。
Namenode中的数据块信息叫作数据块,Datanode中保存的数据块叫作副本。
INodeFile.blocks字段记录了一个HDFS文件拥有的所有数据块,也正是通过这个字段HDFS第一关系与第二关系发生了关联。
INodeFile.blocks字段是一个BlockInfo类型的数组,而BlockInfo是Block的子类,HDFS使用Block类来抽象Namenode中的数据块。
1.Block类
Block类用来唯一标识Namenode中的数据块,实现了Writable接口,可以序列化。还实现了Comparable接口,按照blockid大小排序。
Block类定义了三个字段:
private long blockId; //唯一标识这个Block对象
private long numBytes; //数据块大小
private long generationStamp; //数据块时间戳
2.BlockInfo类
继承自Block类,添加了两个字段:
private BlockCollection bc; //保存该数据块属于哪一个HDFS文件
private Object[] triplets; //保存该数据块的副本存储在哪些数据节点上。
其中,triplets[3 * i]保存的是这个数据块副本的第i个Datanode的DatanodeStorageInfo对象(描述Datanode上存储的对象),而triplets[3 * i+1]为同一个Datanode存储上保存的前一个数据块对应的BlockInfo对象,triplets[3 * i+2]为同一个Datanode存储上保存的后一个数据块对应的BlockInfo对象。像一个双向链表,这样可以节省内存,也可以更方便地找一个Datanode上存的所有BlockInfo对象。
3.BlockInfoUnderConstruction类
继承自BlockInfo类。HDFS在加载fsimage时,如果当前加载的文件处于正在构建状态,则将该INodeFile的最后一个数据块设置为BlockInfoUnderConstruction,表面最后一个数据块正在构建中,而其他的数据块均为正常的BlockInfo。
4.BlocksMap类
BlocksMap类管理着Namenode上数据块的元数据,包括当前数据块属于哪个HDFS文件,以及当前数据块保存在哪些Datanode上。Namenode通过BlocksMap维护数据块副本与数据节点之间的对应关系。
当Datanode启动时,会对Datanode的本地磁盘进行扫描,并将当前Datanode上保存的数据块信息汇报到Namenode。Namenode收到Datanode的汇报信息后,会建立数据块与保存这个数据块的数据节点的对应关系,并将这个信息保存在BlocksMap中。因而获取某个数据块对应的HDFS文件,获取数据块保存在哪些数据节点上,都需要通过BlocksMap对象。
BlocksMap的实现是通过一个GSet对象维护了Block->BlockInfo的映射关系。GSet是Hadoop自己实现的一个有映射功能的集合。维护Block->BlockInfo的映射关系是因为BlockInfo保存数据节点的信息都是在Datanode启动时上报的,而Namenode启动时内存中保存的关于数据块的信息只有Block类中维护的那么多,随着Datanode不断上报数据块信息,将BlockInfo信息通过BlocksMap的映射去更新到Namenode对应的Block中。
5.Block类的状态
Namenode中的数据块有四种状态:
static public enum BlockUCState {
COMPLETE, //完成状态,数据块的长度和时间戳不再发生变化
UNDER_CONSTRUCTION, //构建状态,数据块正在被写入
UNDER_RECOVERY, //恢复状态,数据块正在进行租约恢复和数据块恢复
COMMITTED; //提交状态,表明客户端已经把该数据块的所有数据都发送到了Datanode组成的数据流管道中,且已经收到了ACK响应
}
HDFS没有使用枚举类给出数据块副本的状态,而是通过BlockManager中的数据结构、不同的数据块副本类以及副本所在Datanode的状态来记录数据块副本的状态。
1.BlockManager数据结构
Namenode使用BlockManager类来管理和维护所有与数据块相关的操作。BlockManager中用于保存不同状态数据块副本的数据结构如下:
//损坏的数据块副本集合
final CorruptReplicasMap corruptReplicas = new CorruptReplicasMap();
//等待删除的数据块副本集合
private final InvalidateBlocks invalidateBlocks;
//推迟操作的数据块副本集合
private final Set<Block> postponedMisreplicatedBlocks = Sets.newHashSet();
//多余的数据块副本集合
public final Map<String, LightWeightLinkedSet<Block>> excessReplicateMap =
new TreeMap<String, LightWeightLinkedSet<Block>>();
//等待复制的数据块副本集合
public final UnderReplicatedBlocks neededReplications = new UnderReplicatedBlocks();
//已经生成复制请求的数据块副本
final PendingReplicationBlocks pendingReplications;
上面的数据结构可以看出,很多集合都是hadoop重新定义的。
(1)CorruptRelicasMap类
用于保存损坏的数据块副本集合,其保存的是损坏的数据块副本与保存这个副本的DataNode的对应关系(Block->Datanode的映射关系),同时还保存了这个副本损坏的原因。其底层使用一个TreeMap作为存储的数据结构。
(2)InvalidateBlocks类
用于保存等待删除的数据块副本集合。它使用TreeMap保存了Datanode到该Datanode上所有等待删除的副本集合的映射。使用LightWeightHashSet对象保存一个Datanode上所有等待删除的副本集合,而LightWeightHashSet是Hadoop定义的占用较少内存的HashSet的实现。
(3)UnderReplicatedBlocks类
用于保存所有等待复制的数据块副本集合。它维护了一个优先级队列priorityQueues,而priorityQueues是一个有着5个子队列的列表,每个子队列对应一个优先级。
优先级0:保存需要立刻备份的数据块,该数据块只有一个拷贝,或其拷贝挂了
优先级1:保存副本数极低的数据块,实际副本数与期望副本数的比例小于1:3时,加入该队列
优先级2:保存正处于备份中,但副本数还没达到优先级1队列中比例的数据块
优先级3:数据块副本数量足够,但副本的分布不是很好
优先级4:保存已经损坏的数据块,也就是该数据块对应的所有副本都损坏了
BlockManager会调用chooseUnderReplicatedBlocks()方法从UnderReplicatedBlocks对象中取出blocksToProcess个待复制的数据块,然后选择一个源数据节点和若各目标数据节点生成复制请求。chooseUnderReplicatedBlocks()方法代码如下:
public synchronized List<List<Block>> chooseUnderReplicatedBlocks(int blocksToProcess) {
//初始化返回值列表,保存从每个优先级队列中取出的数据块
List<List<Block>> blocksToReplicate = new ArrayList<List<Block>>(LEVEL);
for(int i = 0; i < LEVEL; i++) {
blocksToReplicate.add(new ArrayList<Block>());
}
//UnderReplicatedBlocks没有保存任何复制数据块
if(size == 0) {
return blocksToReplicate;
}
int blockCount = 0;
//遍历UnderReplicatedBlocks中的所有优先级队列
for(int priority = 0; priority < LEVEL; priority++) {
//当前优先级队列保存的待复制数据块的迭代器
BlockIterator neededReplicationsIterator = iterator(priority);
//获取当前优先级队列的读取偏移值
Integer replIndex = priorityToReplIdx.get(priority);
//从priorityToReplIdx字段记录的读取游标开始读取数据
for(int i = 0; i < replIndex && neededReplicationsIterator.hasNext(); i++) {
neededReplicationsIterator.next();
}
//获取从当前队列中读取的副本数量
blocksToProcess = Math.min(blocksToProcess, size());
//如果读取了足够数量,则退出循环
if(blockCount == blocksToProcess) {
break;
}
//读取副本,并将副本保存到blocksToReplicate返回值列表中
while(blockCount < blocksToProcess && neededReplicationsIterator.hasNext()) {
Block block = neededReplicationsIterator.next();
blocksToReplicate.get(priority).add(block);
replIndex++;
blockCount++;
}
if(!neededReplicationsIterator.hasNext() && neededReplicationsIterator.getPriority() == LEVEL - 1) {
//将所有优先级队列的读取偏移量重置为0,因为最近没有新添加的待复制副本
for(int i = 0; i < LEVEL; i++){
priorityToReplIdx.put(i, 0);
}
break;
}
//更新当前队列的读取游标
priorityToReplIdx.put(priority, replIndex);
}
//返回所有待复制数据块
return blocksToReplicate;
}
(4)PendingReplicationBlocks类
用于存放已经生成复制请求的数据块副本。将已经生成复制请求的数据块副本放入该类缓存,如果出现复制失败的情况,则将该数据块副本重新加入UnderReplicatedBlocks类。
PendingReplicationBlocks类保存了数据块到数据块的复制信息的映射关系Block->PendingBlockInfo,而PendingBlockInfo对象中保存了最近一次复制操作的时间戳,以及正在对当前数据块进行复制操作的数据节点。
数据块副本复制操作执行成功后,Datanode会通知BlockManager对象。BlockManager将这个新添加的副本信息加入内存中。由于副本已经成功地写入数据节点了,所以BlockManager从pendingReplications队列中删除该数据节点上的复制请求。
如果复制操作没有成功,则复制请求会一直保存在pendingReplications字段中,直到复制请求过期。
(5)postponedMisreplicatedBlocks队列
当Namenode发生错误并进行了Active与Standby切换时,Namenode中保存的多余副本不能直接被删除,需要先放入postponedMisreplicatedBlocks队列队列中,直到整个数据块的所有副本所在的Datanode都进行了块汇报。
2.数据块副本状态
数据块副本状态图如下所示:
3.复制和删除操作
computeDatanodeWork()方法执行复制操作和删除操作。代码如下:
int computeDatanodeWork() {
//处于安全模式下不可以进行复制以及删除操作
if(namesystem.isInSafeMode()) {
return 0;
}
//获取集群中所有有效的Datanode的数量
final int numlive = heartbeatManager.getLiveDatanodeCount();
//计算出进行复制操作的数据块数量
final int blocksToProcess = numlive * this.blocksReplWorkMultiplier;
//计算出进行删除操作的Datanode数量
final int nodesToProcess = (int)Math.ceil(numlive * this.blocksInvalidateWorkPct);
//计算出需要进行备份的副本
int workFound = this.computeReplicationWork(blocksToProcess);
...
//计算出需要进行删除的副本
workFound += this.computeInvalidateWork(nodesToProcess);
return workFound;
}
复制操作
复制操作由computeReplicationWork()方法执行,复制步骤为:
1.先从needReplications队列中选出blocksToProcess个需要复制的数据块
2.然后为这些数据块选择源节点source以及目标节点target
3.接下来为数据块生成名字节点指令,通过该指令向Datanode发送复制指令,复制指令通过下一次心跳带回到源节点source
4.Datanode收到心跳带回的复制指令之后,会执行数据块的复制操作,完成数据块的复制操作之后,Datanode会通过增量块汇报接口通知Namenode数据块已经成功复制了。
删除操作
删除操作由computeInvalidateWork()方法执行,删除步骤为:
1.先从invalidateBlocks队列中选出nodesToProcess个Datanode
2.然后在每个Datanode上选择blockInvalidateLimit个副本删除
3.接下来为待删除的副本生成删除指令,通过Datanode的心跳响应将删除指令带回Datanode节点
4.Datanode收到心跳带回的删除指令之后,会执行删除操作,完成数据块的删除操作之后,Datanode会通过增量块汇报接口通知Namenode数据块已经成功删除了。
1.添加数据块
当客户端向HDFS写入新文件时,如果写满了一个数据块,客户端会调用addBlock()方法向Namenode申请一个新的数据块。
这个请求到达Namenode后会由getAdditionalBlock()方法响应。getAdditionalBlock()方法首先会检查文件系统状态,然后为新添加的数据块选择存放副本的Datanode,最后构造Block对象并调用addBlock()方法将Block对象加入文件对应的INode对象中。
addBlock()方法会首先构造Block对应的BlockInfo对象,然后调用addBlockCollection()方法将这个BlockInfo对象加入blocksMap字段中存储,最后addBlock()方法会将BlockInfo对象添加到INodeFile对象的blocks字段中保存。
2.添加副本
当Datanode上写入了一个新的数据块副本或完成了一次数据块副本复制操作后,会向Namenode汇报该Datanode上添加了一个新的数据块副本。向Namenode中添加一个洗的副本后,会引起副本对应数据块状态的改变。在添加副本过程中会调用到addStroedBlock()方法。具体流程如下:
1.addStoredBlock()方法首先确认当前副本是否属于Namenode内存中的一个HDFS文件,如果不属于则直接返回。
2.然后会调用addBlock()方法在数据块与数据节点存储的映射中添加当前数据节点存储的信息,也就是在BlockInfo的triplets[]数组中添加当前DatanodeStroageInfo的信息。并在当前数据节点存储对象上添加这个数据块的信息,也就是在DatanodeStorageInfo的blockList链表中添加当前副本对应的BlockInfo对象。
3.如果新添加的副本对应数据块的状态为COMMITTED,addStoredBlock()方法会调用completeBlock()方法将Namenode中保存的当前数据块的状态由构建状态转换为正常状态。
4.addStoredBlock()方法会调用isNeedReplication()判断当前数据块的副本数量是否满足期望,也就是用户配置的副本系数。如果已经满足了期望,则该数据块没有必要进行复制操作,从neededReplications队列中删除了这个数据块;如果不满足期望,则调用updateNeededReplications()判断数据块需要复制的次数,然后更新neededReplications队列。
5.addStoredBlock()方法还会判断数据块当前的副本数量是否已经超出了期望,如果超出了则存在多余副本,将其放入excessReplicateMap队列中。
6.添加了新的副本之后,如果该数据块的有效副本数量已经超过了期望,addStoredBlock()方法会调用invalidateCorruptReplicas()将该数据块所有的损坏副本从Datanode上删除,也从blocksMap字段删除。
3.删除数据块
当客户端删除一个HDFS文件时,客户端会调用ClientProtocol.delete()删除HDFS文件或目录,并删除文件拥有的所有数据块,以及这个数据块在Datanode上的所有副本。这个请求会由deleteInternal()方法中响应。具体流程如下:
1.deleteInternal()方法首先会调用FSDirectory.delete()方法将文件对应的INode对象从文件系统目录树中删除,然后将这个INode下保存的所有数据块收集到collectedBlocks集合中。然后会调用removeBlocks()方法删除collectedBlocks集合中收集的所有数据块。
2.removeBlocks()方法会遍历collectedBlocks中的所有数据块,然后调用removeBlock()方法将该数据块从Naemnode中完全删除,包括blocksMap、postponedMisreplicatedBlocksCount、pendingReplications、neededReplications、corruptReplicas等队列中保存的该数据块的信息,及其副本信息。
3.之后removeBlocks()会调用addToInvalidates()方法将该数据块的所有副本从Datanode上删除。
4.addToInvalied()方法会遍历所有保存这个数据块副本的数据节点,然后将这个数据节点保存的副本加入invalidateBlocks队列中,然后对该队列执行后续删除操作即可。
4.删除副本
在HDFS文件被删除、副本数量过多、副本损坏这三种情况下会删除副本,数据块副本删除情况如下:
5.数据块的复制
数据块复制操作是HDFS保证数据块冗余存储的一个重要特性,也体现了HDFS故障检测和自动恢复的特性。
Namenode会在客户端完成了一个文件的写操作、更改副本系数、块汇报这三种情况下降一个数据块副本加入neededReplications队列以执行数据块副本复制流程。流程如下图:
Namenode中数据块与数据节点的对应关系不持久化到fsimage文件中,而是由Datanode定期块汇报到Naemnode,然后由Namenode重建内存中数据块与数据节点的对应关系。
Datanode启动后,会与Namenode握手、注册以及向Namenode发送第一次全量块汇报,全量块汇报中包含了Datanode上存储的所有副本信息。之后Datanode以默认6小时的间隔向Namenode发送全量块汇报,同时以100 * 300(默认)秒间隔向Namenode发送增量块汇报,增量块汇报中包含了Datanode最近新添加的以及删除的副本信息。
可以看出,块汇报共有三种类型。第一次的全量块汇报、周期性的全量块汇报、周期性的增量块汇报。
1.第一次的全量块汇报
为了提高HDFS的启动速度,Namenode对于启动时发送的第一次全量块汇报,不会计算哪些元数据需要删除,不会计算无效副本,会将这些处理都推迟到后面的全量块汇报处理。
块汇报到达Namenode之后,Namenode判断是否该数据节点的第一次块汇报,如果是则调用processFirstReport()方法处理,该方法效率很高。其执行步骤如下:
1.首先processFirstReport()方法会调用addStoredBlockImmediate()方法将块汇报中所有有效的副本加入Namenode内存中
2.之后processFirstReport()方法会调用markBlockAsCorrupt()方法处理无效副本。
3.如果是在HDFS HA架构中,Datanode的心跳信息、全量块汇报以及增量块汇报会同时发送到Standby Namenode以及Active Namenode。Standby Namenode处理全量块汇报时,可能出现命名空间还未与Active Namenode同步的情况,此时需要将待处理副本暂时缓存起来,等到Standby Namenode完全加载editlog并更新命名空间后再处理。
第一步的addStoredBlockImmediate()方法是addStoredBlock()的快速版本。其并不考虑underReplication、overReplication、pendingReplications、corruptReplicas等队列的更新操作,也不用记录日志,而是直接在内存中添加这个副本的信息。
addStoredBlockImmediate()方法是通过调用addBlock()方法将数据块副本的信息加入Namenode内存中的。addBlock()方法首先会更新副本对应的BlockInfo对象的triplets[]数组,将当前数据块存储对应的DatanodeStorageInfo对象加入triplets[]数组中,然后将副本对应的BlockInfo添加到DatanodeStorageInfo的blockList队列中。
2.周期性的全量块汇报
对于周期性的全量块汇报,Namenode调用processReport()方法处理。该方法会调用reportDiff()方法,将块汇报中的副本u当前Namenode内存中记录的副本状态做对比,然后产生5个操作队列:
1.toAdd队列:如果上报副本与Namenode内存中记录的数据块有相同的时间戳以及长度,就将上报副本添加到toAdd队列中。对于toAdd队列中的元素,将执行添加副本的操作。
2.toRemove队列:如果副本在Namenode内存中的DatanodeStorageInfo对象上存在,但是块汇报时并没有上报该副本,就将副本添加到toRemove队列中。对于toRemove队列中的元素,将执行删除副本的操作。
3.toInvalidate队列:如果BlockManager的blocksMap字段中没有保存上报副本的信息,就将上报副本添加到toInvalidate队列中。对于toInvalidate队列中的元素,将其加入invalidateBlocks队列,然后触发Datanode节点删除该副本。
4.toCorrupt队列:如果上报副本的时间戳或文件长度不正常,就将上报副本添加到toCorrupt队列中。对于toCorrupt队列中的元素,执行删除损坏副本的操作。
5.toUC队列:如果上报副本对应的数据块处于构建状态,则构造一个ReplicateUnderConstruction对象,然后将该对象添加到reolicas队列中。
3.增量块汇报
Datanode调用blockReceivedAndDeleted()方法将短时间内接收到的副本或者删除的副本增量汇报给Namenode,Namenode收到了增量汇报后,会调用processIncrementalBlockReport()方法处理。
该方法会遍历增量汇报中的所有数据块,如果是新添加的数据块,则调用addBlock()方法处理添加请求。如果是删除的数据块,则调用removeStorageBlock()修改数据块与存储这个数据块的数据节点存储的对应关系。如果是接收中的副本,则调用processAndHandleRreportedBlock()方法处理。
对于增量汇报中新添加的副本,可能是客户端通过输入流管道写入了一个副本,也有可能是Namenode发起的复制操作。addBlock()方法会更改DatanodeDescriptor上的blockScheduled计数,然后从pendingReplications中移除这个数据节点上该数据块的复制请求,最后调用processAndHandleReportedBlock()处理副本为提交状态的数据块副本。
数据节点就是Datanode,Namenode对于数据块与Datanode的映射关系需要在Datanode上报后动态构建。NameNode中有很大一部分逻辑是与Datanode相关的,因此对Datanode的管理是很重要的。
数据节点描述符DatanodeDescriptor是Namenode中对Datanode的抽象,继承自DatanodeInfo类。
1.DatanodeId
DatanodeId用于唯一标识一个Datanode,Datanode通过
2.DatanodeInfo
扩展自DatanodeId,它携带了一些比较简单的Datanode信息,代码如下:
private long capacity; //容量
private long dfsUsed; //使用的空间
private long remaining; //剩余空间
private long blockPoolUsed; //数据块池使用量
private long cacheCapacity; //缓存容量
private long cacheUsed; //缓存使用量
private long lastUpdate; //上次更新时间
private int xceiverCount; //xceiver数量
private String location = NetworkTopology.DEFAULT_RACK; //地址
private String softwareVersion; //软件版本
protected AdminStates adminState; //标识当前Datanode可能处于的状态
3.DatanodeDescriptor
DatanodeDescriptor是Namenode中用于描述一个Datanode信息的类,继承自DdatanodeInfo类,这个类只用在Namenode侧,对于Client是不可见的。
DatanodeDescriptor定义了很多字段,比较重要的有:
1.状态相关:isAlive记录当前Datanode是否有效;decommissioningStatus记录撤销操作时节点的状态;currApproxBlocksScheduled用于估计Ddatanode的负载。
2.指令相关:badnwidth记均衡器带宽;replicateBlocks保存要被复制的数据块以及复制操作的目标Datanode;recoverBlocks保存所有参与恢复过程的Datanode;invalidateBlocks保存要进行删除操作的副本。
3.缓存相关:pendingCached保存所有已经在当前Datanode上等待缓存的数据块;cached保存当前Datanode上已经缓存的数据块;pendingUncached中保存等待取消缓存的数据块。
DatanodeStorageInfo类描述Datanode上的一个存储,一个Datanode可以定义多个存储来保存数据块,且存储可以异构。
在HDFS2.6版本之前,Namenode内存中维护的第二关系是数据块与保存数据块副本的数据节点的对应关系,即Block与DatanodeDescriptor的对应关系。而在HDFS2.6版本中,为支持Datanode的异构存储,Namenode中维护的第二关系变成了数据块与保存数据块副本的数据节点存储的对应关系,即Block与DatanodeStorageInfo的对应关系,汇报的单位也有Datanode变成了DatanodeStorageInfo。
DatanodeStorageInfo中比较重要的字段有下面这些:
1.blockList:用来记录当前存储上保存的数据块副本链表的头节点。当Namenode成功接收一个数据块副本后,Namenode会调用addBlock()在该DatanodeStorageInfo的blockList中添加这个副本对应的BlockInfo对象。
2.heartbeatedSinceFailover:心跳,当Namenode出现失败时被置为false
3.blockContentsStale:标识是否是stale状态的存储,当一个存储时stale状态时,其上所有的副本都是stale状态的,stale状态数据块的所有副本是不可以执行删除操作的。当出现Active与Standby切换时,Namenode会将所有Datanode存储的heartbeatedSinceFailover以及blockContentsStale字段设置为false和true。然后扫描内存中的所有数据块,如果当前数据块有副本为stale状态,就将这个数据块放入postponedMisereplicatedBlocks队列中,直到所有stale状态的Datanode存储进行了块汇报。
4.storage元信息:dn字段是当前存储所在Datanode对应的DatanodeDescriptor对象;storageId是该存储在集群内唯一的标识符;storageType用于描述当前存储时什么类型
当Datanode向Namenode汇报该Datanode存储上接收了一个新的数据块副本时,BlockManager会调用addBlock()方法在Namenode的第二关系中添加这个副本与保存副本的Datanode存储的对应关系。流程如下:
1.addBlock()方法首先调用addStorage()将这个DatanodeStorageInfo对象添加到数据块所属的Datanodes存储列表中,也就是数据块对应的BlockInfo对象的triplets[]数组中。
2.addStorage()方法在triplets[]数组中找到插入当前DatanodeStorageInfo的位置,并插入。
3.然后调用listInsert()将数据块插入到Datanode存储管理的数据块链表中,也就是将BlockInfo对象加入Datanode存储对应的DatanodeStorageInfo对象的blockList链表中。
4.listInsert()方法将当前数据块对应的BlockInfo对象添加到Datanode存储管理的数据块链表中,插入的方法是直接插入在链表的头节点blockList之前,并用新添加数据块的BlockInfo对象替代原有的blockList作为数据块链表的头节点。
DatanodeManager类中记录了在Namenode上注册的Datanode,及这些Datanode在网络中的拓扑结构等信息。其定义的字段比较重要的有:
1.datanodeMap:维护StorageId->DatanodeDescriptor的映射关系
2.host2DataMap:维护host->DatanodeDescriptor的映射关系
3.networktopology:维护整个网络的拓扑结构
HDFS的一个重要特征就是具有弹性,当HDFS需要增加容量时,可以动态地向集群中添加新的Datanode。当HDFS需要减小规模时,可以动态地撤销已经存在的Datanode。
HDFS提供了dfs.hosts文件(又称include文件)以及dfs.hosts.exclude文件管理接入到HDFS的Datanode。include文件制定了可以连接到Namenode的Datanode列表,exclude文件指定了不能连接到Namenode的Datanode列表。
HDFS管理员将一个Datanode添加到集群中时,需要在include文件中添加一条该Datanode的记录,然后调用"dfsadmin -refreshNodes"命令刷新名字节点信息,最后才能启动 Datanode。同理,撤销节点通过exclude文件,管理员将要撤销的节点信息添加到exclude文件中,也是调用"dfsadmin -refreshNodes"命令,Namenode就会开始撤销节点操作。被撤销节点上的数据块会被复制到集群中的气体Datanode上,在这个过程中Datanode处于“正在撤销状态”,数据复制完成后Datanode状态会转变为“已撤销”,这时就可以关闭Datanode了。
执行"dfsadmin -refreshNodes"命令最终是由refreshNodes()方法响应。refreshNodes()方法首先调用refreshHostsReader()方法将include文件与exclude文件加载到hostFileManager对象中,之后调用refreshDatanodes()方法刷新所有的数据节点。
refreshDatanodes()方法会遍历datanodeMap字段中保存的所有DatanodeDescriptor对象。对于不可以连接到Namenode的Datanode,设置isAllowed字段为false,表示该Datanode不可以接入HDFS集群。对于exclude文件中的节点,需要进行撤销操作,调用startDecommission()开始撤销操作。
startDecommission()方法首先将当前Datanode对应的adminState设置为正在撤销状态,之后调用checkDecommissionState()检查撤销操作是否完成。如果完成则将adminState设置为已经撤销的状态。checkDecommissionState()方法会判断当前节点上保存的 所有数据块是否满足副本系数,不满足则将数据块加入neededReplications进行复制操作。
Datanode启动时,需要与Namenode进行握手、注册和数据块上报三个操作。
1.握手操作
由DatanodeProtocol的versionRequest()方法实现,它直接返回命名空间的信息。
2.注册
由DatanodeProtocol的registerDatanode()方法实现。Datanode会为注册的Datanode分配唯一的storageId作为标识。Datenode的注册情况有三种:
(1)该Datanode没有注册过
(2)该Datanode注册过,但这次注册使用了新的storageId,表明该数据节点的存储空间已经被清理过,原来的数据块副本都被删除了
(3)该Datanode注册过,这次是重复注册
第一种情况下,将该新注册的Datanode添加到datanodeMap以及host2DatanodeMap中,然后更新网络拓扑,检查节点是否撤销。
第二种情况下,要清理Namenode中这个Datanode的信息,然后再当作一个新的Datanode按第一种情况处理。
第三种情况下,此时只需要刷新注册的信息更新Namenode内存中保存的Datanode原有信息。
3.数据块上报
由DatanodeProtocol的processReport()方法实现。就是一次块汇报,要区分是不是第一次块汇报。
Datanode会以默认3秒的间隔向Namenode发送心跳。心跳信息包括:Datanode的注册信息、Datanode的存储信息、缓存信息、当前Datanode写文件的连接数、读写数据使用的线程数等信息。
Namenode收到心跳后,会返回一个心跳响应。心跳响应中包含一个DatanodeCommand的数组,同来携带Namenode对Datanode的指令,如数据块副本的复制、删除、缓存等指令。
Namenode还会周期性检测所有Datanode上报心跳的情况,对于长时间没有上报心跳的Datanode,则任务该Datanode出现故障不能正常工作,会删除该数据节点。
对Datanode发来的心跳请求处理分心跳新处理和心跳检测检查两部分。
1.心跳信息处理
Datanode发送的心跳信息由handleHeartbeat()方法处理。其处理步骤如下:
1.对发送心跳的数据节点进行检查。检查该数据节点是否能连接到Namenode,如果不能则抛出异常;检查该节点是否在Namenode上注册过,如果没有注册,则发出指令让Datanode重新发起注册请求。
2.Namenode从Datanode的心跳中取出负载信息,调用updateHeartbeat()方法更新整个集群的负载信息,同时也更新了节点的心跳事件。
3.Namenode为Datanode生成名字节点指令,如果当前名字节点还处于安全模式中,则返回空指令,否则依次生成数据块恢复指令、数据块复制指令、数据块删除指令、缓存相关指令、balancer带宽指令。最后将指令返回给Datanode。
2.心跳检查
HeartbeatManager类会定期调用heartbeatCheck()方法检查所有数据节点是否更新了心跳并执行了清理操作,间隔默认是5分钟。
在heartbeatCheck()方法中,如果发现Datanode在timeout的时间内还未上报心跳,则认为Datanode发生故障。对于故障的Datanode,将其从Namenode中删除这个数据节点的信息,以及这个数据节点保存的所有数据块副本信息。
同时,heartbeatCheck()方法会查找一个故障的Datanode存储,对于故障的Datanode存储,会删除故障Datanode存储上的所有数据块副本信息。