namenode作为hdfs中的元数据的管理模块,免不了会提到元数据包括哪些?在内存中又是如何存储管理的,本文就来聊聊nn内存中的元数据信息。
【整体概况】
在HDFS中,NN的主要作用是元数据管理,包括文件系统目录树的管理、block块的管理、以及dn结点管理。
在具体实现中,FSDirectory对应文件系统目录树的管理,包括文件系统中各个目录、文件信息记录,以及彼此之间的层级关系;
DatanodeManager管理所有注册的dn结点信息,同时根据dn的心跳信息、块汇报更新相关的信息记录。
BlockManager则是nn中最庞大的一部分,内部又拆分成多个类来存储管理块的不同信息和状态。例如使用blocksmap存储所有的块信息,使用underReplicatedBlocks、PendingReplicationBlocks来保证块的副本数始终满足指定个数。
虽然这几个部分分别管理不同的元数据信息,但彼此并不是孤立的,而是相互联系在一起的,例如:
-
一个文件中需要知道自身数据存储在哪些块中,具体实现中以一个数组记录该文件包含的所有块信息,因此文件和块就关联起来了
-
同样,一个块的副本分别存储在哪个dn结点的哪个卷目录中,也需要记录下来。这样,当dn结点出现异常,或者dn结点上的卷出现异常时,nn需要对存储在这个dn结点上的块进行副本的迁移保证块的副本数满足指定个数。这样,块信息就和dn的存储信息关联起来了。
下面就这几部分内容分别展开说明。
【文件系统目录树】
在HDFS文件系统中,一个目录或者一个文件,都以一个INode来表示,并且都具有一些相同的属性,例如权限、所属用户/用户组、最后修改时间、最后访问时间等;但两者又有一些不同的地方,例如目录下可以有子目录或文件,而文件则没有这些东西,但文件是有实际存储的数据的,这些数据存储在一个或多个块中。
在具体实现中:
-
类INodeDirectory对应存储文件系统中的目录信息,INodeFile对应存储文件系统中的文件信息;
-
两个类均继承自抽象类INodeWithAdditionFields,在这个抽象类中,定义了一些通用的属性(文件/目录的名字、权限、等),feature接口数组包含目录或文件一些扩展的特性,诸如配额、快照、访问控制、扩展属性等;
-
抽象类INode是所有文件/目录的父类,该类中有一个类成员parent,记录文件/目录的父节点,一方面是可以快速找到每个目录/文件的父目录,另一方面是这些信息会持久化到fsimage中,启动时会根据记录的parent字段,重新在内存中构造出一颗完整的文件目录树;
-
在INodeDirectory中,类成员child是一个INode的集合,记录该目录下的子目录(不包含递归子目录)和文件;
-
在INodeFile中,类成员blocks是一个BlockInfo的数组,记录该文件的内容对应存储的块信息,即文件内容由哪些块组成;
-
FSDirectory类记录整个文件系统目录树的信息,其类成员rootDir对应文件系统中的根目录("/"),类成员inodemap是INodeMap类的实例对象,内部的map包含了所有的inode信息。
对相关类有了了解后,文件系统目录树层级结构和内存中相关类的对应关系就能比较清晰的描述出来了。
【dn信息的存储】
dn节点信息的几个基础类(如下图所示):
-
DatanodeID
记录了datanode的一些基本信息,包括唯一标示dn节点的ID(uuid)、dn的rpc通信ip,端口、用于数据传输的ip,端口等(图中没有列出全部字段)。
-
DatanodeInfo
继承自DatanodeID,扩展记录了dn的可用容量、当前已使用容量、用于数据传输的线程数等信息。
-
DatanodeDescriptor
继承自DatanodeInfo,记录了该dn上不同卷目录下存储的块信息、异常卷目录的个数及概况、以及nn需要通过心跳下发给dn的一些任务信息,这些任务按不同类型分别存储,例如需要进行块复制的任务;块恢复的任务;块删除的任务。
-
DatanodeStorageInfo
dn节点某个卷的详细信息,包括卷的状态、存储ID、存储类型(ssd,hdd等)、存储容量、hdfs数据存储占用空间、非hdfs数据存储占用空间、以及存储在该卷下的所有块组成的双向链表的表头等。
-
DatanodeManager
如字面意思,dn节点信息的管理类,内部以一个map表存储所有dn节点的信息,网络拓扑架构,以及一些其他用于节点管理控制的信息。
【block的存储】
块信息的存储主要包括几个基础类(如下图所示):
-
Block
用来描述记录一个块信息,算是所有块信息描述的基类。自身实现了writable接口,可以进行序列化,方便在rpc中进行传输。
-
BlockInfo
块信息的补充描述,继承自Block类,但本身是一个抽象类。
nn内部大部分数据结构中都以BlockInfo作为基础,存储块的相关信息。
几个重要的类成员:replication记录块创建时指定的副本数;triplets记录了块的所有副本对应存储的dn信息;uc则记录了块的状态以及块所有副本的状态。
-
BlockInfoContigous
继承自BlockInfo,由于BlockInfo是一个抽象类,因此在nn的内存中,真正存储块信息的是该类的实例对象及引用。
-
BlockUnderConstructionFeature
描述了块的状态、以及块所有副本的信息
-
ReplicaUnderConstruction
块的副本的信息,包括副本具体存储的dn卷信息,副本的状态等。
上面几个类的作用主要就是用来描述nn中存储的块信息,在这个基础上,还定义了一些类用于块的存储和管理:
1)BlocksMap
内部以一个LightWeightGSet(本质上是一个哈希表)存储所有的块信息。
2)CorruptReplicasMap
内部以嵌套map的方式存放损坏的块信息,其中key为Block,表示是哪个块;
value同样为一个map,嵌套map中的key为块所属dn节点,value为损坏的原因,例如缺少时间戳、缺少长度、无效的状态、客户端或dn上报等。
3)InvalidateBlocks
存放无效的、待删除的块信息。内部同样定义了一个map,其中key为DatanodeInfo,value为存储在该dn节点上的BlockInfo集合。
4)UnderReplicatedBlocks
存放不满足副本数的块信息,内部一个数组加链表的方式来存储块信息。
5)PendingReplicationBlocks
当前正在进行块复制的块信息、以及块复制任务超时的块信息。
【block的状态变化】
上面提到了块的状态,一个块的生命周期中会有如下几个状态:
-
underConstruction(简称uc)
-
committed
-
complete
-
underRecovery
初始状态为underConstruction,然后依次经过committed,最终变为complete。
underRecovery则用于块恢复。
一个块由一个或多个副本组成,每个副本也有自己的状态,副本的状态包括:
-
RBW(Replica Being Write)
-
RWR(Replica Waiting to be Recovered)
-
RUR(Replica is Under Recovery)
-
FINALIZED
-
TEMPORARY
对于正常写流程,副本的状态只会从RBW转换为FINALIZED。
在完整的写流程中,一个块的状态及其副本的状态的可能变化如下图所示:
1. 客户端请求一个新的块时,nn在内部为其分配一个块,块的初始状态为uc,此后为该block近一步选择副本存储的dn结点,最后在内存中依次将块的副本状态初始化为RBW。
2. 此后客户端向dn建立连接,并开始写block数据,在写的过程中,dn会通过增量块汇报向nn上报块的状态,包括正在接收块(receiving)和已经接收完成(received),对于已经接收完成的块,nn在处理时,会找到块对应的副本,将其状态置为finalized。
3. 客户端写完一个块后(收到dn结点的ack响应),会继续向nn申请新的块,在申请新的块请求时,请求中会携带前一个块的信息。nn在处理请求时,默认对前一个块执行commit动作,即将块的状态设置为committed。此后,再近一步判断,当前块副本的状态为finalized的个数是否达到创建时指定的副本数,如果是,则将块的状态设置为complete。
4. 随着更多副本存储的dn结点进增量块汇报后,该块的副本数都处于finalized状态,这时,如果块的状态为committed,nn会将块的状态置为complete。也就是说,只有客户端对隐式对块进行committed后,你能在处理dn的谮量块汇报过程中也会触发状态的切换。
5. 上面讲到了nn的最终状态为complete,但图中箭头最后指向的blockInfo中状态却是null。这就是代码中的一个小技巧了,直接将对象置为null来表示块的complete状态,因为complete已经是最终状态了,在内存中再开辟内存空间记录其状态没有意义,索性直接置为null。这对于需要存储几千万到亿级别的块信息来说,无疑节省了不少内存。
【block与dn的关联】
在上面提到的DatanodeStorageInfo类中,有一个blockList的字段,该字段是BlockInfo的一个实例对象,表示在该卷上存储的一个块信息。
而BlockInfo中的triplets字段是一个对象数组,数组长度为块副本数✖️3,即每个副本占用3个位置,分别记录该副本所在的卷信息(DatanodeStroageInfo),以及前一个块信息,后一个块信息。
如上图所示,DatanodeStorageInfo中的blockList作为链表的表头,然后通过triplets中每个副本占用的两个位置(prev,next),将存储在该卷中的块串联起来,形成一个双向链表。
这样在nn内部不仅可以快速知道一个块存储在哪个dn的哪个卷目录下,还可以快速获取一个dn的一个卷目录下存储了哪些块。
这对于dn节点的上下线,或者dn节点的卷目录异常后的块恢复很重要,方便快速定位哪些块需要进行块复制或者块恢复。
【总结】
本文主要讲解了nn中几个元数据信息在内存中如何进行存储的,包括文件系统的目录树结构、datanode节点信息、块信息。
当然还有也很多没有展开的,例如,nn内部如何保证块的副本数始终维持在指定个数的,即块副本数的监测,块复制、块删除任务的触发执行,以及块是如何恢复的,还有一些知识点没有提到,例如dn节点的网络拓扑,机架感知等。后续再单独进行讲解。。
好了,本文就介绍到这里,如果觉得本文对你有些帮助,来个点赞,在看吧,也欢迎分享转发, 谢谢~