漫画大数据:HDFS 中 NameNode 的内存为什么会一直涨?
以下内容参考自:https://tech.meituan.com/2016/08/26/namenode.html
NameNode 管理着整个 HDFS 文件系统的元数据。从架构设计上看,元数据大致分成两个层次:
图 1
除了对文件系统本身元数据的管理之外,NameNode 还需要维护整个集群的机架及 DataNode 的信息、Lease 管理以及集中式缓存引入的缓存管理等等。这几部分数据结构空间占用相对固定,且占用较小。
NameNode 整个内存结构大致可以分成四大部分:Namespace、BlocksMap、NetworkTopology 及其它,图 2 为各数据结构内存逻辑分布图示。
图 2
NameNode 常驻内存主要被 Namespace 和 BlockManager 使用,二者使用占比分别接近 50%。其它部分内存开销较小且相对固定,与 Namespace 和 BlockManager 相比基本可以忽略。
NameNode 通过 NetworkTopology 维护整个集群的树状拓扑结构,当集群启动过程中,通过机架感知(通常都是外部脚本计算)逐渐建立起整个集群的机架拓扑结构,一般在 NameNode 的生命周期内不会发生大变化。拓扑结构的叶子节点 DatanodeDescriptor 是标识 DataNode 的关键结构,该类继承关系如下图所示:
DatanodeDescriptor 继承关系
在 64 位 JVM 中,DatanodeDescriptor 内存使用情况如下图所示(除特殊说明外,后续对其它数据结构的内存使用情况分析均基于 64 位 JVM):
DatanodeDescriptor 内存使用详解
由于 DataNode 节点一般会挂载多块不同类型存储单元,如 HDD、SSD 等,图 2 中 storageMap 描述的正是存储介质 DatanodeStorageInfo 集合,其详细数据结构如下图所示:
DatanodeStorageInfo 内存使用详解
除此之外,DatanodeDescriptor 还包括一部分动态内存对象,如 replicateBlocks、recoverBlocks 和 invalidateBlocks 等与数据块动态调整相关的数据结构,pendingCached、cached 和 pendingUncached 等与集中式缓存相关的数据结构。由于这些数据均属动态的形式临时存在,随时会发生变化,所以这里没有做进一步详细统计(结果存在少许误差)。
根据前面的分析,假设集群中包括 2000 个 DataNode 节点,NameNode 维护这部分信息需要占用的内存总量:
(64 + 114 + 56 + 109 ∗ 16)∗ 2000 = ~4MB
在树状机架拓扑结构中,除了叶子节点 DatanodeDescriptor 外,还包括内部节点 InnerNode 描述集群拓扑结构中机架信息。
NetworkTopology 拓扑结构内部节点内存使用详解
对于这部分描述机架信息等节点信息,假设集群包括 80 个机架和 2000 个 DataNode 节点,NameNode 维护拓扑结构中内部节点信息需要占用的内存总量:
(44 + 48) ∗ 80 + 8 ∗ 2000 = ~25KB
从上面的分析可以看到,为维护集群的拓扑结构 NetworkTopology,当集群规模为 2000 时,需要的内存空间不超过 5MB,按照接近线性增长趋势,即使集群规模接近 10000,这部分内存空间~25MB,相比整个 NameNode JVM 的内存开销微乎其微。
与传统单机文件系统相似,HDFS 对文件系统的目录结构也是按照树状结构维护,NameSpace 保存的正是整个目录树及目录树上每个目录 /
文件节点的属性,包括:名称(name)
,编号(id)
,所属用户(user)
,所属组(group)
,权限(permission)
,修改时间(mtime)
,访问时间(atime)
,子目录/文件(children)
等信息。
下图 3 为 Namespace 中 INode 的类图结构,从类图可以看出,文件 INodeFile 和目录 INodeDirectory 的继承关系。其中目录在内存中由 INodeDirectory 对象来表示,并用 List children
成员列表来描述该目录下的子目录或文件;文件在内存中则由 INodeFile 来表示,并用 BlockInfo[] blocks
数组表示该文件由哪些 Blocks 组成。其它属性由继承关系的各个相应子类成员变量标识。
图 3
目录和文件结构在继承关系中各属性的内存占用情况如图 4 所示:
图 4
除图中提到的属性信息外,一些附加如 ACL 等非通用属性,没有在统计范围内。在默认场景下,INodeFile
和 INodeDirectory.withQuotaFeature
是相对通用和广泛使用到的两个结构。
根据前面的分析,假设 HDFS 目录和文件数分别为 1 亿,Block 总量在 1 亿情况下,整个 Namespace 在 JVM 中内存使用情况:
Total(Directory) = (24 + 96 + 44 + 48) ∗ 100M + 8 ∗ num(total children)
Total(Files) = (24 + 96 + 48) ∗ 100M + 8 ∗ num(total blocks)
Total = (24 + 96 + 44 + 48) ∗ 100M + 8 ∗ num(total children) + (24 + 96 + 48) ∗ 100M + 8 ∗ num(total blocks) = ~38GB
关于预估方法的几点说明:
Namespace 在 JVM 堆内存空间中常驻,在 NameNode 的整个生命周期一直在内存存在,同时为保证数据的可靠性,NameNode 会定期对其进行 Checkpoint,将 Namespace 物化到外部存储设备。随着数据规模的增加,文件数 / 目录树也会随之增加,整个 Namespace 所占用的 JVM 内存空间也会基本保持线性同步增加。
HDFS 将文件按照一定的大小切成多个 Block,为了保证数据可靠性,每个 Block 对应多个副本,存储在不同 DataNode 上。NameNode 除需要维护 Block 本身的信息外,还需要维护从 Block 到 DataNode 列表的对应关系,用于描述每一个 Block 副本实际存储的物理位置,BlockManager 中 BlocksMap 结构即用于 Block 到 DataNode 列表的映射关系。BlocksMap 内部数据结构如图 5 所示。
图 5
BlocksMap 经过多次优化形成当前结构,最初版本直接使用 HashMap 解决从 Block 到 BlockInfo 的映射。由于在内存使用、碰撞冲突解决和性能等方面存在问题,之后使用重新实现的 LightWeightGSet 代替 HashMap,该数据结构本质上也是利用链表解决碰撞冲突的 HashTable,但是在易用性、内存占用和性能等方面表现更好。关于引入 LightWeightGSet 细节可参考 HDFS-1114。
与 HashMap 相比,为了尽可能避免碰撞冲突,BlocksMap 在初始化时直接分配整个 JVM 堆空间的 2% 作为 LightWeightGSet 的索引空间,当然 2% 不是绝对值,如果 2% 内存空间可承载的索引项超出了 Integer.MAX_VALUE/8(注:Object.hashCode () 结果是 int,对于 64 位 JVM 的对象引用占用 8Bytes)会将其自动调整到阈值上限。限定 JVM 堆空间的 2% 基本上来自经验值,假定对于 64 位 JVM 环境,如果提供 64GB 内存大小,索引项可超过 1 亿,如果 Hash 函数适当,基本可以避免碰撞冲突。
BlocksMap 的核心功能是通过 BlockID 快速定位到具体的 BlockInfo,关于 BlockInfo 详细的数据结构如图 6 所示。BlockInfo 继承自 Block,除了 Block 对象中 BlockID,numbytes 和 timestamp 信息外,最重要的是该 Block 物理存储所在的对应 DataNode 列表信息 triplets。
图 6
其中 LightWeightGSet 对应的内存空间全局唯一。尽管经过 LightWeightGSet 优化内存占用,但是 BlocksMap 仍然占用了大量 JVM 内存空间,假设集群中共 1 亿 Block,NameNode 可用内存空间固定大小 128GB,则 BlocksMap 占用内存情况:
16 + 24 + 2% ∗ 128GB +( 40 + 128 )∗ 100M = ~20GB
BlocksMap 数据在 NameNode 整个生命周期内常驻内存,随着数据规模的增加,对应 Block 数会随之增多,BlocksMap 所占用的 JVM 堆内存空间也会基本保持线性同步增加。
假设 HDFS 中,目录数量为 num_directory
,文件数量为 num_files
,所有文件对应的 block 总数为 num_blocks
,
NameSpace 中,目录信息所占用的内存总量可近似计算为:
Total(Directory) = 212 Byte ∗ num_directory + 8 Byte ∗ (num_directory + num_files)
= 220 Byte * num_directory + 8 Byte * num_files
文件信息所占用的内存总量可近似计算为:
Total(Files) = 168 Byte ∗ num_files + 8 Byte ∗ num_blocks
BlockMap 所占用的内存总量可近似计算为:
Total(BlockMap) = 40 Byte + 2% ∗ NameNode堆内存 + 168 Byte ∗ num_blocks
由于 NameSpace 和 BlockMap 几乎占满了 NameNode 的内存,所以一般估算 NameNode 所需内存时,还可以近似为:
Total = Total(Directory) + Total(Files) + Total(BlockMap)
~= 200 Byte * (num_directory + num_files + num_blocks) + 2% * NameNode堆内存
—————END—————
文中「澜妹、澜宝」使用了数澜的吉祥物,点击获取 业界首张《数据中台产品技术能力地图》!