HDFS由NameNode和DataNode组成,其中NameNode作为Master节点,负责维护整个集群的状态,为了提高响应速度其大部分数据都常驻内存,则NameNode内存的使用尤为重要,一旦NameNode出现故障,整个Hadoop集群就将处于不可服务的状态。
在解析NameNode内存之前先来回顾下HDFS整体架构。
从上图中可以看出HDFS可以分为两层,分别为NameSpace和Block Storage Service,其中Block Storage Service包含Block Management和Storage两部分。
NameSpace和Block Management在NameNode中,Storage在DataNode中。
NameSpace主要存储集群的目录结构和文件所对应的block映射(file->blocks的映射),是HDFS文件系统实际执行的核心,提供各种增删改查文件操作接口。
Block Management
通过注册和周期性的心跳提供dn集群成员
处理dn的block report,存储在BlocksMap中
提供block相关的操作,如create、delete、modif和get block location
管理replica的放置策略,不足副本因子的replica进行复制,超过副本因子的replica进行删除
Storage
由DataNode提供,用于将replica存放在本地文件系统中并提供读写权限。
由上得知NameNode中主要由NameSpace和Block Management组成,其中NameSpace和BlocksMap是占内存的大户。
NameNode的内存中除了上面提到的NameSpace和BlocksMap之外,还有维护整个集群拓扑结构的NetworkTopology、管理整个集群写租约的LeaseManager、管理集中式缓存的CacheManager和SnapshotManager。NameNode的内存结构如下图:
Namespace:维护整个文件系统的目录树结构,及目录树上的状态变化;
BlocksManager:维护整个文件系统中与数据块相关的信息,及数据块的状态变化;
NetworkTopology:维护机架拓扑及DataNode信息,机架感知的基础;
LeaseManager:读写的互斥同步就是靠Lease实现,支持HDFS的Write-Once-Read-Many的核心数据结构;
CacheManager:Hadoop 2.3.0引入的集中式缓存新特性,支持集中式缓存的管理,实现memory-locality提升读性能;
SnapshotManager:Hadoop 2.1.0引入的Snapshot新特性,用于数据备份、回滚,以防止因用户误操作导致集群出现数据问题;
DelegationTokenSecretManager:管理HDFS的安全访问;
其他:临时数据信息、统计信息metrics等等。
NameNode常驻内存主要被Namespace和BlockManager使用,二者使用占比分别接近50%。其他部分内存开销较小且相对固定,与Namespace和BlockManager相比基本可以忽略。
与单机文件系统相似,HDFS对文件系统的目录结构也是按照树状结构维护,Namespace保存了目录树及每个目录/文件节点的属性。除在内存常驻外,这部分数据会定期flush到持久化设备上,生成一个新的FsImage文件,方便NameNode发生重启时,从FsImage及时恢复整个Namespace。
在整个Namespace目录树中存在两种不同类型的INode数据结构:INodeDirectory和INodeFile。其中INodeDirectory标识的是目录树中的目录,INodeFile标识的是目录树中的文件。由于二者均继承自INode,所以具备大部分相同的公共信息INodeWithAdditionalFields,除常用基础属性外,其中还提供了扩展属性features,如Quota,Snapshot等均通过Feature增加,如果以后出现新属性也可通过Feature方便扩展。不同的是,INodeFile特有的标识副本数和数据块大小组合的header(2.6.1之后又新增了标识存储策略ID的信息)及该文件包含的有序Blocks数组;INodeDirectory则特有子节点的列表children。这里需要特别说明children是默认大小为5的ArrayList,按照子节点name有序存储,虽然在插入时会损失一部分写性能,但是可以方便后续快速二分查找提高读性能,对一般存储系统,读操作比写操作占比要高。
BlocksMap在NameNode的内存空间中占据很大的比例,由BlockManager统一管理。相比NameSpace,BlockManager管理的这部分数据要复杂的多。Namespace与BlockManager之间通过前面提到的INodeFile有序Blocks数组关联到一起。下图是BlockManager管理的内存结构。
INodeFile是一个实体file在NameNode内存中的一个对象,包含一个存放block信息的BlockInfo数组,数组的大小为文件block的个数。如上图BlockInfo[A~K]所示。
BlockInfo继承自Block,维护的是Block的元数据,除基本信息外还包括一个inode引用(private BlockCollection bc),表示该block所属的文件;以及一个记录replica到底存放在那些dn上的三元组数组Object[] triplets,大小为3*replicas,其中replicas是Block副本数量。triplets包含的信息:
triplets[3i]:Block所在的DataNode A;(DatanodeStorageInfo对象)
triplets[3
i+1]:该DataNode A上前一个Block;(指向前一个block的BlockInfo对象引用)
triplets[3*i+2]:该DataNode A上后一个Block;(指向后一个block的BlockInfo对象引用)
其中i表示的是Block的第i个副本,i取值[0,replicas)。
从前面描述中通过BlockInfo可以得到以下几块重要信息:
文件包含了哪些Block(由BlockInfo对该文件的引用可以得到该文件所有的block信息)
这些Block分别被实际存储在哪些DataNode上(通过BlockInfo的triplets数组可以得到该block的replica存储位置)
DataNode上所有Block前后链表关系(通过BlockInfo的triplets数组中pre)
如果从信息完整度来看,以上数据足够支持所有关于HDFS文件系统的正常操作,但还存在一个使用场景较多的问题:不能通过blockid快速定位Block,所以引入了BlocksMap。
BlocksMap底层通过LightWeightGSet实现(关于LightWeightGSet的详细介绍可参考这篇blog),本质是一个链式解决冲突的哈希表。为了避免rehash过程带来的性能开销,初始化时,索引空间直接给到了整个JVM可用内存的2%,并且不再变化。集群启动过程,DataNode会进行BR(BlockReport),根据BR的每一个Block计算其HashCode,之后将对应的BlockInfo插入到相应位置逐渐构建起来巨大的BlocksMap。前面在INodeFile里也提到的BlockInfo集合,如果我们将BlocksMap里的BlockInfo与所有INodeFile里的BlockInfo分别收集起来,可以发现两个集合完全相同,事实上BlocksMap里所有的BlockInfo就是INodeFile中对应BlockInfo的引用;通过Block查找对应BlockInfo时,也是先对Block计算HashCode,根据结果快速定位到对应的BlockInfo信息。
前面提到部分都属于静态数据部分,NameNode内存中所有数据都要随读写情况发生变化,BlockManager当然也需要管理这部分动态数据。主要是当Block发生变化不符合预期时需要及时调整Blocks的分布。这里涉及几个核心的数据结构:
excessReplicateMap:若某个Block实际存储的副本数多于预设副本数,这时候需要删除多余副本,这里多余副本会被置于excessReplicateMap中。excessReplicateMap是从DataNode的StorageID到Block集合的映射集。
neededReplications:若某个Block实际存储的副本数少于预设副本数,这时候需要补充缺少副本,这里哪些Block缺少多少个副本都统一存在neededReplications里,本质上neededReplications是一个优先级队列,缺少副本数越多的Block之后越会被优先处理。
invalidateBlocks:若某个Block即将被删除,会被置于invalidateBlocks中。invalidateBlocks是从DataNode的StorageID到Block集合的映射集。如某个文件被客户端执行了删除操作,该文件所属的所有Block会先被置于invalidateBlocks中。
corruptReplicas:有些场景Block由于时间戳/长度不匹配等等造成Block不可用,会被暂存在corruptReplicas中,之后再做处理。
关于这几个数据结构在维持副本平衡中的更多内容,可以移步到这篇blog
前面几个涉及到Block分布情况动态变化的核心数据结构,这里的数据实际上是过渡性质的,BlocksManager内部的ReplicationMonitor线程(关于ReplicationMonitor的内容可以查看blog会持续从其中取出数据并通过逻辑处理后分发给具体的DatanodeDescriptor对应数据结构,当对应DataNode的心跳过来之后,NameNode会遍历DatanodeDescriptor里暂存的数据,将其转换成对应指令返回给DataNode,DataNode收到任务并执行完成后再反馈回NameNode,之后DatanodeDescriptor里对应信息被清除。如BlockB预设副本数为3,由于某种原因实际副本变成4(如之前下线的DataNode D重新上线,其中B正好有BlockB的一个副本数据),BlockManager能及时发现副本变化,并将多余的DataNode D上BlockB副本放置到excessReplicateMap中,ReplicationMonitor线程定期检查时发现excessReplicateMap中数据后将其移到DataNode D对应DatanodeDescriptor中invalidateBlocks里,当DataNode D下次心跳过来后,随心跳返回删除Block B的指令,DataNode D收到指令实际删除其上的Block B数据并反馈回NameNode,此后BlockManager将DataNode D上的Block B从内存中清除,至此Block B的副本符合预期,整个流程如下所示。
前面多次提到Block与DataNode之间的关联关系,事实上NameNode确实还需要管理所有DataNode,不仅如此,由于数据写入前需要确定数据块写入位置,NameNode还维护着整个机架拓扑NetworkTopology。下图所示内存中机架拓扑图。
从图中可以看出这里包含两个部分:机架拓扑结构NetworkTopology和DataNode节点信息。其中树状的机架拓扑是根据机架感知(一般都是外部脚本计算得到)在集群启动完成后建立起来,整个机架的拓扑结构在NameNode的生命周期内一般不会发生变化;另一部分是比较关键的DataNode信息,BlockManager已经提到每一个DataNode上的Blocks集合都会形成一个双向链表,更准确的应该是DataNode的每一个存储单元DatanodeStorageInfo上的所有Blocks集合会形成一个双向链表,这个链表的入口就是机架拓扑结构叶子节点即DataNode管理的DatanodeStorageInfo。此外由于上层应用对数据的增删查随时发生变化,随之DatanodeStorageInfo上的Blocks也会动态变化,所以NetworkTopology上的DataNode对象还会管理这些动态变化的数据结构,如replicateBlocks/recoverBlocks/invalidateBlocks,这些数据结构正好和BlockManager管理的动态数据结构对应,实现了数据的动态变化由BlockManager传达到DataNode内存对象最后通过指令下达到物理DataNode实际执行的流动过程。
这里存在一个问题,为什么DatanodeStorageInfo下所有Block之间会以双向链表组织,而不是其他数据结构?如果结合实际场景就不难发现,对每一个DatanodeStorageInfo下Block的操作集中在快速增加/删除(Block动态增减变化)及顺序遍历(BlockReport期间),所以双向链表是非常合适的数据结构。
这里只简单介绍下Lease机制,更多内容请查看blogHDFS租约解析
Lease机制是重要的分布式协议,广泛应用于各种实际的分布式系统中。HDFS支持Write-Once-Read-Many,对文件写操作的互斥同步靠Lease实现。Lease实际上是时间约束锁,其主要特点是排他性。客户端写文件时需要先申请一个Lease,一旦有客户端持有了某个文件的Lease,其他客户端就不可能再申请到该文件的Lease,这就保证了同一时刻对一个文件的写操作只能发生在一个客户端。NameNode的LeaseManager是Lease机制的核心,维护了文件与Lease、客户端与Lease的对应关系,这类信息会随写数据的变化实时发生对应改变。
从上图中可以看出LeaseManager内存结构中主要包括以下三个核心数据结构:
sortedLeases:Lease集合,按照时间先后进行排序,便于检查Lease是否超时(hardLimit);
leases:客户端到Lease的映射关系;(一对一,一个clent对应一个lease)
sortedLeasesByPath:文件路径到Lease的映射关系;(多对一,多个path对应一个lease)
其中每一个写数据的客户端会对应一个Lease,每个Lease里包含至少一个标识文件路径的Path。Lease本身已经维护了其持有者(客户端)及该Lease正在操作的文件路径集合,之所以增加了leases和sortedLeasesByPath为提高通过Lease持有者或文件路径快速索引到Lease的性能。
由于Lease本身的时间约束特性,当Lease发生超时后需要强制回收,内存中与该Lease相关的内容要被及时清除。超时检查及超时后的处理逻辑由LeaseManager.Monitor统一执行。LeaseManager中维护了两个与Lease相关的超时时间:软超时(softLimit)和硬超时(hardLimit),使用场景稍有不同。
正常情况下,客户端向集群写文件前需要向NameNode的LeaseManager申请Lease;写文件过程中定期更新Lease时间,以防Lease过期,周期与softLimit相关;写完数据后申请释放Lease。整个过程可能发生两类问题:
1、写文件过程中客户端没有及时更新Lease时间;
2、写完文件后没有成功释放Lease。
两个问题分别对应为softLimit和hardLimit。两种场景都会触发LeaseManager对Lease超时强制回收。如果客户端写文件过程中没有及时更新Lease超过softLimit时间后,另一客户端尝试对同一文件进行写操作时触发Lease软超时强制回收;如果客户端写文件完成但是没有成功释放Lease,则会由LeaseManager的后台线程LeaseManager.Monitor检查是否硬超时后统一触发超时回收。不管是softLimit还是hardLimit超时触发的强制Lease回收,处理逻辑都一样:FSNamesystem.internalReleaseLease,逻辑本身比较复杂,这里不再展开(详细内容可以查看blog),简单的说先对Lease过期前最后一次写入的Block进行检查和修复,之后释放超时持有的Lease,保证后面其他客户端的写入能够正常申请到该文件的Lease。
NameNode内存数据结构非常丰富,这里对几个重要的数据结构进行了简单的描述,除了前面罗列之外,其实还有如SnapShotManager/CacheManager等,由于其内存占用有限且有一些特性还尚未稳定,这里不再展开
NameNode的内存主要由NameSpace和BlocksMap占用,其中NameSpace存储的主要是INodeFile和INodeDirectory对象,BlocksMap存储的主要是BlockInfo对象。则估算NameNode占用的内存大小也就是估算集群中INodeFile、INodeDirectory和BlockInfo这些对象占用的heap空间。
Java中常见数据结构占用的内存大小
下面先列举下java中常见数据结构占用的内存大小(64bit的jvm)
int = 4 bytes
long = 8 bytes
Reference size(引用) = 8 bytes
Object header size(对象头) = 16 bytes
Array header size(数组头) = 24 bytes
ArrayList header size(list头) = 24(数组头) + 4(属性size的大小) = 28 bytes
TreeMap.Entry = 64 bytes. (Entry的属性中有5个引用)
HashMap.Entry = 48 bytes. (Entry的属性有3个引用)
String header = 64 bytes.
则对INodeFile、INodeDirectory和BlockInfo对象进行大小评估还有一点疑惑,看了几篇资料还是有点不清晰,这里就不展开了,有兴趣的可以看文章末尾的资料,随后搞清楚了会进行更新。
但参考了很多资料如Hadoop权威指南、HADOOP-1687等,INodeFile、INodeDirectory和BlockInfo对象大小在150~200bytes之间,可以使用150bytes对其内存进行评估,
例子:
NameNode文件和block信息如下:
94115032 files and directories, 91722740 blocks = 185837772 total filesystem object(s).
memSum = 185837772 * 150 bytes = 27875665800bytes = 25.9612368g < 26g
使用jmap命令查看此时NameNode的内存
命令jmap -histo:live pid > mem,输出内存大小为24619484976(22.9286821g,大约23g)
小插曲
使用命令jmap -heap pid查看nn所占的内存时,发现nn占的内存比估算的26g要大很多,随后使用histo命令之后才发现和估算的差不多,原因可能是使用heap查看的nn占用的内存,其中可能包括一些无用的对象(在等待gc),而使用histo查看的是nn中存活的对象,能够更好的展示评估的结果。
如果是2亿的FileSystem object则内存空间大约为30g(200M * 150 = 30000M,约等30G)
本篇文章大部分内容是从网上整理所得,只有少部分是原创,所涉及的资料在参考一节。