DatanodeProtocol是Datanode与Namenode间的接口,Datanode会使用这个接口与Namenode握手、注册、发送心跳、进行全量及增量数据块汇报。Namenode会在Datanode的心跳响应中携带Namenode指令,Datanode收到Namenode指令后会执行对应的操作。注意,Namenode向Datanode下发Namenode指令是没有其他任何接口的,只会通过DatanodeProtocol的返回值来下发命令
DatanodeProtocol定法的方法如下图所示
这些方法可以分为三种类型,Datanode启动相关、心跳相关及数据块读写相关
Datanode启动相关方法
一个完整的Datanode启动操作会与Namenode进行4次交互,也就是调用4次DatanodeProtocol定义的方法。首先调用versionRequest()与Namenode进行握手操作,然后调用registerDatanode()向Namenode注册当前的Datanode,接着调用blockReport()汇报Datanode上存储的所有数据块,最后调用cacheReport()汇报Datanode缓存的所有数据块。
首先看下versionRequest()方法,方法定义如下
public NamespaceInfo versionRequest() throws IOException;
Datanode启动会首先调用versionRequest()方法与Namenode进行握手。这个方法的返回值是一个NamespaceInfo对象,NamespaceInfo对象会封装当前HDFS集群的命名空间信息,包括存储系统的布局版本号(layoutversion)、当前的命名空间ID(namespaceId)、集群ID(clusterId)、文件系统的创建时间(ctime)、构建时的HDFS版本号(buildVersion)、块池ID(blockpoolId)、当前的软件版本号(softwareVersion)等。Datanode获取到NamespaceInfo对象后,就会比较Datanode当前的HDFS版本号和Namenode的HDFS版本号,如果Datanode版本与Namenode版本不能协同工作,则抛出异常,Datanode也就无法注册到该Namenode上。如果当前Datanode上已经有了文件存储的目录,那么Datanode还会检查Datanode存储上的块池ID、文件系统ID、以及集群ID与Namenode返回的是否一致。
成功握手后,Datanode会调用registerDatanode()方法向Namenode注册当前的Datanode,该方法定义如下
public DatanodeRegistration registerDatanode(DatanodeRegistration registration
) throws IOException;
这个方法的参数是一个DatanodeRegistration对象。它封装了DatanodeID、Datanode的存储系统的布局版本号(layoutversion)、当前的命名空间ID(namespaceId)、集群ID(clusterId)、文件系统的创建时间(ctime)以及Datanode当前的软件版本号(softwareVersion)。Namenode会判断Datanode的软件版本号与Namenode的软件版本号是否兼容,如果兼容则进行注册操作,并返回一个DatanodeRegistration对象供Datanode后续处理逻辑使用。
Datanode成功向Namenode注册之后,Datanode会调用blockReport()方法向Namenode上报它管理的所有数据块的信息。该方法定义如下
public DatanodeCommand blockReport(DatanodeRegistration registration,
String poolId, StorageBlockReport[] reports) throws IOException;
这个方法需要三个参数,DatanodeRegistration用于标识当前的Datanode,poolId用于标识数据块所在的块池ID,reports是一个StorageBlockReport对象的数组,每个StorageBlockReport对象都用于记录Datanode上一个存储空间存储的数据块。注意,上报的数据块是以长整形数组保存的,每个已经提交的数据块(finalized)以3个长整型来表示,每个构建中的数据块(under-construction)以4个长整型来表示。之所以不使用ExtendedBlock对象保存上报的数据块,是因为这样可以减少blockReport()操作所使用的内存,Namenode接收到消息时,不需要创建大量的ExtendedBlock对象,只需要不断地从长整型数组中提取数据块即可。
Namenode接收到blockReport()请求之后,会根据Datanode上报的数据块存储情况建立数据块与数据节点之间的对应关系。同时,Namenode会在blockReport()的响应中携带Namenode指令,通知Datanode进行重新注册、发送心跳、备份或者删除Datanode本地磁盘上数据块副本操作。这些Namenode节点指令都是以DatanodeCommand对象封装的。
blockReport()方法只在Datanode启动时以及指定间隔时执行一次。在这里间隔是由dfs.blockreport.intervalMsec参数配置的,默认是6小时执行一次。cacheReport()方法与blockReport()方法是完全一致的,只不过汇报的是当前Datanode上缓存的书友数据块。
cacheReport()方法定义如下
public DatanodeCommand cacheReport(DatanodeRegistration registration,
String poolId, List<Long> blockIds) throws IOException;
心跳相关方法
分布式系统的节点之间大多采用心跳来维护节点的健康状态。HDFS也是一样的,Datanode会定期(有dfs.heartbeat.interval配置项配置,默认是3秒)向Namenode发送心跳,如果Namenode长时间没有接到Datanode发送的心跳,则Namenode会认为该Datanode失效
DatanodeProtocol的sendHeartbeat()方法就是用于心跳汇报的接口。该方法定义如下
public HeartbeatResponse sendHeartbeat(DatanodeRegistration registration,
StorageReport[] reports,
long dnCacheCapacity,
long dnCacheUsed,
int xmitsInProgress,
int xceiverCount,
int failedVolumes) throws IOException;
该方法除了携带标识Datanode身份的DatanodeRegistration对象外,还包括数据节点上所有存储的状态、缓存的状态、正在写文件数据的连接数、读写数据使用的线程数等。
sendHeartbeat()会返回一个HeartbeatResponse对象,这个对象包含了Namenode向Datanode发送的Namenode指令,以及当前Namenode的HA状态。注意,在开启了HA的HDFS集群中,Datanode是需要同时向Active Namenode以及Standby Namenode发送心跳的,不过只有Active Namenode才能向Datanode下发Namenode指令
数据块读写相关方法
Datanode在进行数据块读写操作时与Namenode交互的方法包括reportBadBlocks()、blockReceivedAndDeleted()、commitBlockSynchronization()三个方法。
reportBadBlocks()与ClientProtocol.reportBadBlocks()方法类似,定义如下
public void reportBadBlocks(LocatedBlock[] blocks) throws IOException;
Datanode会调用这个方法向Namenode汇报损坏的数据块,Datanode会在三种情况下调用这个方法
- DataBlockScanner线程定期扫描数据节点上存储的数据块,发现数据块的校验出现错误时
- 数据流管道写数据时,Datanode接收了一个新的数据块,进行数据块校验操作出现错误时
- 进行数据块复制操作(DataTransfer),Datanode读取本地存储的数据块时,发现本地数据块副本的长度小于Namenode记录的长度,则认为该块数据已经无效
reportBadBlocks()方法的参数是LocatedBlock对象,这个对象描述了出现错误数据块的位置,Namenode收到reportBadBlocks()请求后,会下发数据块副本删除指定删除错误的数据块。
Datanode会定期(默认是5分钟,不可以配置)调用blockReceivedAndDeleted()方法向Namenode汇报Datanode新接受的数据块或者删除的数据块。blockReceivedAndDeleted()方法定义如下
public void blockReceivedAndDeleted(DatanodeRegistration registration,
String poolId,
StorageReceivedDeletedBlocks[] rcvdAndDeletedBlocks)
throws IOException;
Datanode接收一个数据块,可能是因为Client写入了新的数据块,或者从别的Datanode上复制一个数据块到当前Datanode。
Datanode删除一个数据块,则有可能是因为该数据块的副本数量过多,Namenode向当前Datanode下发了删除数据块副本的指令。
可以把blockReceivedAndDeleted()方法理解为blockReport()的增量汇报,这个方法的参数包括DatanodeRegistration对象、增量汇报数据块所在的块池ID,以及StorageReceivedDeletedBlocks对象的数组,这里StorageReceivedDeletedBlocks对象封装了Datanode的一个数据存储上新添加以及删除的数据块集合。Namenode接收了这个请求之后,会更新它内存中数据块与数据节点的对应关系
commitBlockSynchronization()方法用于租约恢复操作时同步数据块的状态,commitBlockSynchronization()方法定义如下
public void commitBlockSynchronization(ExtendedBlock block,
long newgenerationstamp, long newlength,
boolean closeFile, boolean deleteblock, DatanodeID[] newtargets,
String[] newtargetstorages) throws IOException;
在恢复租约操作时,主数据节点完成所有租约恢复协调操作后调用commitBlockSynchronization()方法同步Datanode和Namenode上所有数据块的状态。
其他方法
DatanodeProtocol中最后一个方法是errorReport(),该方法用于向Namenode上报运行过程中发生的一些状况,如磁盘不可用等,这个方法在调试时非常有用
DatanodeCommand
g sendHeartbeat()、blockReport()以及cacheReport()方法的返回值都会携带Namenode向Datanode下发的Namenode指令。在HDFS中,使用DatanodeCommand类描述Namenode向Datanode发出的Namenode指令
DatanodeCommand类及其子类结构如图
DatanodeCommand是所有Namenode节点的基类,一共有7个子类,但在DatanodeProtocol中一共定义了10个Namenode指令,每个指令都有一个唯一的编号与之对应,代入如下
final static int DNA_UNKNOWN = 0; // unknown action 未定义
final static int DNA_TRANSFER = 1; // transfer blocks to another datanode 数据块复制
final static int DNA_INVALIDATE = 2; // invalidate blocks 数据块删除
final static int DNA_SHUTDOWN = 3; // shutdown node 关闭datanode
final static int DNA_REGISTER = 4; // re-register 重新注册datanode
final static int DNA_FINALIZE = 5; // finalize previous upgrade 提交上一次升级
final static int DNA_RECOVERBLOCK = 6; // request a block recovery 数据块恢复
final static int DNA_ACCESSKEYUPDATE = 7; // update access key 安全相关
final static int DNA_BALANCERBANDWIDTHUPDATE = 8; // update balancer bandwidth 更新平衡器带宽
final static int DNA_CACHE = 9; // cache blocks 缓存数据块
final static int DNA_UNCACHE = 10; // uncache blocks 解缓存数据块
DatanodeProtocol中定义的指令类型数量和DatanodeCommand子类的数量并不一致,这是因为指令编号DNA_SHUTDOWN已经废弃不用了,Datanode接收到DNA_SHUTDOWN指令后会直接抛出UnsupportedOperationException异常。关闭Datanode是通过ClientDatanodeProtocol.shutdownDatanode()方法来触发的
DNA_TRANSFER、DNA_RECOVERBLOCK以及DNA_INVALIDATE都是通过BlockCommand子类来封装的,只不过参数不同。
DNA_TRANSFER指令用于触发Datanode的数据块复制操作,当HDFS系统中某个数据块的副本数小于配置的副本系数时,Namenode会通过DNA_TRANSFER指令通知某个拥有这个数据块的副本的Datanode将该数据块复制到其他Datanode上。
DNA_INVALIDATE用于通知Datanode删除Datanode上的指定数据块,这是因为Namenode发现了某个数据块的副本数已经超过了配置的副本系数,这时Namenode会通知某个Datanode删除这个Datanode上多余的数据块副本。
当客户端在写文件时发生异常退出,会造成数据流管道中不同Datanode上数据块状态的不一致,这时Namenode会从数据流管道中选出一个数据节点作为主恢复节点,协调数据流管道中的其他数据节点进行租约恢复操作,以同步这个数据块的状态。此时Namenode就会向这个Datanode下发DNA_RECOVERBLOCK指令,通知数据节点开始租约恢复操作。