Hexiaoqiao

Focus on BigData,Distributed System,Hadoop 2.*

HDFS升级过程中重启方式选择

一、背景

集群在运行过程中,由于升级等原因难免会遇到重启NameNode或整个集群节点的情况,不同的重启方式会影响到整个运维操作的效率。
刚接触HDFS时的某次全集群内DataNode升级,遇到一次非预期内的NameNode重启。升级时,NameNode首先进入Safemode模式,全集群禁止写操作。DataNode数据包和配置更新后操作了所有DataNode一次性重启,之后NameNode间歇性不能响应,持续高负载达~45min,之后不得不通过重启NameNode,之后~35min全集群启动完成,服务恢复正常。

由此引出问题:
1、造成全集群DataNode重启后NameNode不能正常响应的根本原因是什么?
2、重启NameNode为什么能够实现恢复服务?

二、原因分析

1、现场还原

在集群重启过程中,不管以什么方式进行重启,避免不了DataNode向NameNode进行BlockReport的交互,从NameNode现场截取两个时间段里部分BlockReport日志。

表1 不同重启方式NameNode处理BR时间统计

DN Restart Only - - With NN Restart - -
Date Blocks ProcessTime Date Blocks ProcessTime
2015-04-28 13:15:51 49428 646 msecs 2015-04-28 14:00:57 66392 73 msecs
2015-04-28 13:15:51 49428 646 msecs 2015-04-28 14:00:57 66392 73 msecs
2015-04-28 13:15:51 49068 644 msecs 2015-04-28 14:00:57 69978 84 msecs
2015-04-28 13:15:52 51822 638 msecs 2015-04-28 14:00:57 78222 98 msecs
2015-04-28 13:15:53 83131 1214 msecs 2015-04-28 14:00:57 64663 71 msecs
2015-04-28 13:15:53 90088 169 msecs 2015-04-28 14:00:57 85106 99 msecs
2015-04-28 13:15:54 82024 1107 msecs 2015-04-28 14:00:57 87346 96 msecs
2015-04-28 13:15:55 48114 637 msecs 2015-04-28 14:00:57 87802 96 msecs
2015-04-28 13:15:55 49457 84 msecs 2015-04-28 14:00:57 65646 71 msecs
2015-04-28 13:15:56 82989 457 msecs 2015-04-28 14:00:57 71025 86 msecs
2015-04-28 13:15:57 84634 1181 msecs 2015-04-28 14:00:57 66144 73 msecs
2015-04-28 13:15:58 67321 885 msecs 2015-04-28 14:00:57 72652 90 msecs
2015-04-28 13:15:59 70668 924 msecs 2015-04-28 14:00:57 66118 76 msecs
2015-04-28 13:15:59 73114 138 msecs 2015-04-28 14:00:58 67011 74 msecs
2015-04-28 13:15:59 28215 692 msecs 2015-04-28 14:00:58 78216 84 msecs
2015-04-28 13:16:00 30080 321 msecs 2015-04-28 14:00:58 60988 66 msecs
2015-04-28 13:16:00 30435 329 msecs 2015-04-28 14:00:58 52376 58 msecs
2015-04-28 13:16:00 34350 360 msecs 2015-04-28 14:00:58 66801 73 msecs
2015-04-28 13:16:01 32487 344 msecs 2015-04-28 14:00:58 49134 53 msecs
2015-04-28 13:16:01 28244 308 msecs 2015-04-28 14:00:58 66928 73 msecs
2015-04-28 13:16:01 29138 308 msecs 2015-04-28 14:00:58 75560 82 msecs
2015-04-28 13:16:02 29765 301 msecs 2015-04-28 14:00:58 83880 92 msecs
2015-04-28 13:16:02 28699 309 msecs 2015-04-28 14:00:58 82989 93 msecs
2015-04-28 13:16:02 35377 370 msecs 2015-04-28 14:00:58 56210 60 msecs
2015-04-28 13:16:03 49204 626 msecs 2015-04-28 14:00:58 65517 78 msecs
2015-04-28 13:16:03 27554 438 msecs 2015-04-28 14:00:58 76159 78 msecs
2015-04-28 13:16:04 27285 326 msecs 2015-04-28 14:00:58 59725 58 msecs

左半部分记录的是重启全集群DataNode后,NameNode处理单个BlockReport请求耗时,右半部分为重启NameNode后,处理单个BlockReport请求耗时。这里只列了部分数据,虽不具统计意义,但是在处理时间的量级上可信。

从数据上可以看到,对于BlockReport类型的RPC请求,不同的重启方式,RPC的处理时间有明显差异。

2、深度分析

前面也提到从数据上看,对于BlockReport类型的RPC请求,重启全集群DataNode与重启NameNode,RPC处理时间有一个数量级的差别。这种差别通过代码得到验证。

BlockManager.javaGithub
1
2
3
4
5
6
7
if (storageInfo.getBlockReportCount() == 0) {
  // The first block report can be processed a lot more efficiently than
  // ordinary block reports.  This shortens restart times.
  processFirstBlockReport(node, storage.getStorageID(), newReport);
} else {
  invalidatedBlocks = processReport(node, storage, newReport);
}

可以看到NameNode对BlockReport的处理方式仅区别于是否为初次BlockReport。初次BlockReport显然只发生在NameNode重启期间。
processFirstBlockReport:对Standby节点(NameNode重启期间均为Standby),如果汇报的数据块相关元数据还没有加载,会将报告的块信息暂存队列,当Standby节点完成加载相关元数据后,再处理该消息队列; 对第一次块汇报的处理比较特别,为提高处理效率,仅验证块是否损坏,然后判断块状态是否为FINALIZED状态,如果是建立块与DN节点的映射,其他信息一概暂不处理。
processReport:对于非初次块汇报,处理逻辑要复杂很多;对报告的每个块信息,不仅会建立块与DN的映射,还会检查是否损坏,是否无效,是否需要删除,是否为UC状态等等。

初次块汇报的处理逻辑单独拿出来,主要原因有两方面:
1、加快NameNode的启动时间;统计数据也能说明,初次块汇报的处理时间比正常块汇报的处理时间能节省约一个数量级的时间。
2、由于启动过程中,不提供正常读写服务,所以只要确保正常数据(整个Namespace和所有FINALIZED状态Blocks)无误,无效和冗余数据处理完全可以延后到IBR或next BR。
说明:
1、是否选择processFirstBlockReport处理逻辑不会因为NameNode当前为safemode或者standby发生变化,仅NameNode重启生效;
2、BlockReport的处理时间与DataNode数据规模正相关,当前DataNode中Block数处于:200,000 ~ 1,000,000。
如果不操作NameNode重启,BlockReport处理时间会因为处理逻辑复杂带来额外的处理时间,统计数据显示,约一个数量级的差别。

NameNode对非第一次BlockReport的复杂处理逻辑只是NameNode负载持续处于高位的诱因,在其诱发下发生了一系列“滚雪球”式的异常放大。
1、所有DataNode进程被关闭后,NameNode的CallQueue(默认大小:3200)会被快速消费完;


2、所有DataNode进程被重启后,NameNode的CallQueue会被迅速填充,主要来自DataNode重启后正常流程里的VersionRequest和registerDataNode两类RPC请求,由于均较轻量,所以也会被迅速消费完;


3、之后DataNode进入BlockReport流程,NameNode的CallQueue填充内容开始从VersionRequest和registerDataNode向BlockReport过渡;


直到CallQueue里几乎被所有BlockReport填充满。


前面的统计数据显示,NameNode不重启对BlockReport的处理时间~500ms,另一个关键数据是Client看到的RPC超时时间,默认为60s;在默认的RPC超时时间范围内,CallQueue里最多可能被处理的BlockReport数~120个,其它均会发生超时。 当发生超时后,Client端(DataNode)会尝试重试,所以NameNode的CallQueue会被持续打满;另一方面,如果NameNode发现RPC Request出现超时会被忽略(可以从日志证实),直到存在未超时的请求,此时从CallQueue拿出来的BlockReport请求虽未超时,但也处于即将超时的边缘,即使处理完成其中的少数几个,CallQueue中的剩余大部分也会出现超时。

namenode.lgo
1
2
3
4
5
6
7
8
9
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.45.38:37649 Call#650 Retry#0
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.53.5:14839 Call#659 Retry#0
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.62.5:55833 Call#702 Retry#0
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.51.31:41016 Call#655 Retry#0
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 29 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.62.36:53163 Call#702 Retry#0
2015-04-28 13:14:48,709 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.46.32:53530 Call#662 Retry#0
2015-04-28 13:14:48,710 INFO org.apache.hadoop.ipc.Server: IPC Server handler 29 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.55.11:52372 Call#662 Retry#0
2015-04-28 13:14:48,710 INFO org.apache.hadoop.ipc.Server: IPC Server handler 9 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.55.44:30295 Call#666 Retry#0
2015-04-28 13:14:48,710 INFO org.apache.hadoop.ipc.Server: IPC Server handler 29 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.16.50.4:37880 Call#674 Retry#0

通过前面的分析,从整个Timeline上看,NameNode长期处于满负荷运行状态,但是有效处理能力非常低(仅针对BlockReport)。这也是为什么1000+ DataNode(每一个DataNode管理的Block数均未超过1,000,000),也即1000+有效BlockReport请求,在~50min内依然没有被处理完成。

如果DataNode进程处于正常运行状态下,重启NameNode后会发生完全不同的情况。
1、NameNode重启后,首先加载FsImage,此时,除Namespace外NameNode的元数据几乎为空,此后开始接收DataNode过来的RPC请求(绝大多数为Heartbeat);


2、NameNode接收到Heartbeat后由于在初始状态会要求DataNode重新注册;由于Heartbeat间隔是3s,所以从NameNode的角度看,所有DataNode的后续一系列RPC请求会被散列到3s时间线上;


3、DataNode向NameNode注册完成后立即开始BlockReport;由于步骤2里提到的3s时间线散列关系,队列里后半部分BlockReport请求和VersionRequest/registerDataNode请求会出现相互交叉的情况;


4、如前述,处理BlockReport时部分RPC请求一样会发生超时;


5、由于超时重试,所以部分BlockReport和registerDataNode需要重试;可以发现不同于重启所有DataNode时重试的RPC几乎都是BlockReport,这里重试的RPC包括了VersionRequest/registerDataNode(可以从日志证实),这就大幅降低了NameNode的负载,避免了“滚雪球”式高负载RPC堆积,使异常有效收敛。

namenode.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.32.73.39:16329 Call#2893 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.32.20.15:54831 Call#2889 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.62.38:10818 Call#2835 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.52.18:59462 Call#2818 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.registerDatanode from 10.16.39.24:13728 Call#2864 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.32.27.8:58789 Call#2883 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.registerDatanode from 10.32.73.40:56606 Call#2889 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.40.21:19961 Call#2843 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.43.13:22644 Call#2870 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.registerDatanode from 10.16.43.26:16289 Call#2876 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.61.30:31968 Call#2825 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.32.21.5:47752 Call#2879 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.blockReport from 10.32.49.11:46892 Call#2904 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.36.24:12326 Call#2859 Retry#0
2015-04-28 14:01:19,302 INFO org.apache.hadoop.ipc.Server: IPC Server handler 31 on 8020: skipped org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 10.16.56.4:55321 Call#2833 Retry#0

3、避免重启大量DataNode时雪崩

从前面的分析过程,可以得出两个结论:
(1)NameNode对正常BlockReport处理效率是造成可能雪崩的根本原因;
(2)BlockReport的堆积让问题完全失控;

从这两个结论出发可以推导出相应的解决办法:

1、解决效率问题:
(1)优化代码逻辑;这块代码相对成熟,可优化的空间不大,另外所需的时间成本较高,暂可不考虑;
(2)降低BlockReport时数据规模;NameNode处理BR的效率低主要原因还是每次BR所带的Block规模过大造成,所以可以通过调整Block数量阈值,将一次BlockReport分成多盘分别汇报,提高NameNode处理效率。可参考的参数为:dfs.blockreport.split.threshold,默认为1,000,000,当前集群DataNode上Block规模数处于240,000 ~ 940,000,建议调整为500,000;另一方面,可以通过在同一个物理节点上部署多个DataNode实例,分散数据,达到缩小规模的目的,但是这种方案仅能解决当前问题,长期来看依然不能避免,且影响范围比较大,需要多方面权衡。

2、解决堆积问题:
(1)控制重启DataNode的数量;按照当前节点数据规模,如果大规模重启DataNode,可采取滚动方式,以~15/单位间隔~1min滚动重启,如果数据规模增长,需要适当调整实例个数;
(2)定期清空CallQueue;如前述,当大规模DataNode实例被同时重启后,如果不采取措施一定会发生“雪崩”,若确实存在类似需求或场景,可以通过定期清空CallQueue(dfsadmin -refreshCallQueue)的方式,避免堆积效应;这种方案的弊端在于不能有选择的清空RPC Request,所以当线上服务期时,存在数据读写请求超时、作业失败的风险。

3、选择合适的重启方式:
(1)当需要对全集群的DataNode重启操作,且规模较大(包括集群规模和数据规模)时,建议在重启DataNode进程之后将NameNode重启,避免前面的“雪崩”问题;
(2)当灰度操作部分DataNode或者集群规模和数据规模均较小时,可采取滚动重启DataNode进程的方式;

三、总结

1、重启所有DataNode时,由于处理BlockReport逻辑不同,及由此诱发的“雪崩式”效应,导致重启进度极度缓慢;
2、在数据规模达到10K~100K,重启一台DataNode都会给NameNode的正常服务造成瞬时抖动;
3、在数据规模到100K量级时,同时重启~15以内DataNode不会对集群造成雪崩式灾难,但是可能出现短时间内服务不可用状态;
4、全集群升级时,建议NameNode和DataNode均重启,在预期时间内可恢复服务。

NameNode内存全景

一、概述

从整个HDFS系统架构上看,NameNode是其中最重要、最复杂也是最容易出现问题的地方,而且一旦NameNode出现故障,整个Hadoop集群就将处于不可服务的状态,同时随着数据规模和集群规模的持续增长,很多小量级时被隐藏的问题逐渐暴露出来。所以,从更高层次掌握NameNode的内部结构和运行机制尤其重要。除特别说明外,本文主要基于社区版本Hadoop-2.4.1[1][2]。

NameNode管理着整个HDFS文件系统的元数据。从架构设计上看,元数据大致分成两个层次:Namespace管理层,负责管理文件系统中的树状目录结构以及文件与数据块的映射关系;块管理层,负责管理文件系统中文件的物理块与实际存储位置的映射关系BlocksMap,如图1所示[1]。Namespace管理的元数据除内存常驻外,也会周期Flush到持久化设备上FsImage文件;BlocksMap元数据只在内存中存在;当NameNode发生重启,首先从持久化设备中读取FsImage构建Namespace,之后根据DataNode的汇报信息重新构造BlocksMap。这两部分数据结构是占据了NameNode大部分JVM Heap空间。

HDFS结构图

除了对文件系统本身元数据的管理之外,NameNode还需要维护整个集群的机架及DataNode的信息、Lease管理以及集中式缓存引入的缓存管理等等。这几部分数据结构空间占用相对固定,且占用较小。

测试数据显示,Namespace目录和文件总量到2亿,数据块总量到3亿后,常驻内存使用量超过90GB。

二、内存全景

如前述,NameNode整个内存结构大致可以分成四大部分:Namespace,BlocksMap,NetworkTopology,LeaseManager & SnapshotManager & CacheManager及其他,图2为各数据结构内存逻辑分布图示。

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相比基本可以忽略。

三、内存分析

3.1 Namespace

与单机文件系统相似,HDFS对文件系统的目录结构也是按照树状结构维护,Namespace保存了目录树及每个目录/文件节点的属性。除在内存常驻外,这部分数据会定期flush到持久化设备上,生成一个新的FsImage文件,方便NameNode发生重启时,从FsImage及时恢复整个Namespace。图3所示为Namespace内存结构。前述集群中目录和文件总量即整个Namespace目录树中包含的节点总数,可见Namespace本身其实是一棵非常巨大的树。

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有序存储,虽然在插入时会损失一部分写性能,但是可以方便后续快速二分查找提高读性能,对一般存储系统,读操作比写操作占比要高。具体的继承关系见图4所示。

INode继承关系

3.2 BlockManager

BlocksMap在NameNode内存空间占据很大比例,由BlockManager统一管理,相比Namespace,BlockManager管理的这部分数据要复杂的多。Namespace与BlockManager之间通过前面提到的INodeFile有序Blocks数组关联到一起。图5所示BlockManager管理的内存结构。

BlockManager管理的内存结构

每一个INodeFile都会包含数量不等的Block,具体数量由文件大小及每一个Block大小(默认为64M)比值决定,这些Block按照所在文件的先后顺序组成BlockInfo数组,如图5所示的BlockInfo[A~K],BlockInfo维护的是Block的元数据,结构如图6所示,数据本身是由DataNode管理,所以BlockInfo需要包含实际数据到底由哪些DataNode管理的信息,这里的核心是名为triplets的Object数组,大小为3*replicas,其中replicas是Block副本数量。triplets包含的信息:

triplets[i]:Block所在的DataNode;
triplets[i+1]:该DataNode上前一个Block;
triplets[i+2]:该DataNode上后一个Block;

其中i表示的是Block的第i个副本,i取值[0,replicas)。

BlockInfo继承关系

从前面描述可以看到BlockInfo几块重要信息:文件包含了哪些Block,这些Block分别被实际存储在哪些DataNode上,DataNode上所有Block前后链表关系。

如果从信息完整度来看,以上数据足够支持所有关于HDFS文件系统的正常操作,但还存在一个使用场景较多的问题:不能通过blockid快速定位Block,所以引入了BlocksMap。

BlocksMap底层通过LightWeightGSet实现,本质是一个链式解决冲突的哈希表。为了避免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信息。至此涉及到HDFS文件系统本身元数据的问题基本上已经解决了。

前面提到部分都属于静态数据部分,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中,之后再做处理。

前面几个涉及到Block分布情况动态变化的核心数据结构,这里的数据实际上是过渡性质的,BlocksManager内部的ReplicationMonitor线程(图5标识Thread/Monitor)会持续从其中取出数据并通过逻辑处理后分发给具体的DatanodeDescriptor对应数据结构(3.3 NetworkTopology里会有简单介绍),当对应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的副本符合预期,整个流程如图7所示。

副本数异常时处理过程

3.3 NetworkTopology

前面多次提到Block与DataNode之间的关联关系,事实上NameNode确实还需要管理所有DataNode,不仅如此,由于数据写入前需要确定数据块写入位置,NameNode还维护着整个机架拓扑NetworkTopology。图8所示内存中机架拓扑图。

NetworkTopology内存结构

从图8可以看出这里包含两个部分:机架拓扑结构NetworkTopology和DataNode节点信息。其中树状的机架拓扑是根据机架感知(一般都是外部脚本计算得到)在集群启动完成后建立起来,整个机架的拓扑结构在NameNode的生命周期内一般不会发生变化;另一部分是比较关键的DataNode信息,BlockManager已经提到每一个DataNode上的Blocks集合都会形成一个双向链表,更准确的应该是DataNode的每一个存储单元DatanodeStorageInfo上的所有Blocks集合会形成一个双向链表,这个链表的入口就是机架拓扑结构叶子节点即DataNode管理的DatanodeStorageInfo。此外由于上层应用对数据的增删查随时发生变化,随之DatanodeStorageInfo上的Blocks也会动态变化,所以NetworkTopology上的DataNode对象还会管理这些动态变化的数据结构,如replicateBlocks/recoverBlocks/invalidateBlocks,这些数据结构正好和BlockManager管理的动态数据结构对应,实现了数据的动态变化由BlockManager传达到DataNode内存对象最后通过指令下达到物理DataNode实际执行的流动过程,流程在3.2 BlockManager已经介绍。

这里存在一个问题,为什么DatanodeStorageInfo下所有Block之间会以双向链表组织,而不是其他数据结构?如果结合实际场景就不难发现,对每一个DatanodeStorageInfo下Block的操作集中在快速增加/删除(Block动态增减变化)及顺序遍历(BlockReport期间),所以双向链表是非常合适的数据结构。

3.4 LeaseManager

Lease 机制是重要的分布式协议,广泛应用于各种实际的分布式系统中。HDFS支持Write-Once-Read-Many,对文件写操作的互斥同步靠Lease实现。Lease实际上是时间约束锁,其主要特点是排他性。客户端写文件时需要先申请一个Lease,一旦有客户端持有了某个文件的Lease,其他客户端就不可能再申请到该文件的Lease,这就保证了同一时刻对一个文件的写操作只能发生在一个客户端。NameNode的LeaseManager是Lease机制的核心,维护了文件与Lease、客户端与Lease的对应关系,这类信息会随写数据的变化实时发生对应改变。

LeaseManager的内存数据结构

图9所示为LeaseManager内存结构,包括以下三个主要核心数据结构:

sortedLeases:Lease集合,按照时间先后有序组织,便于检查Lease是否超时;
leases:客户端到Lease的映射关系;
sortedLeasesByPath:文件路径到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,逻辑本身比较复杂,这里不再展开,简单的说先对Lease过期前最后一次写入的Block进行检查和修复,之后释放超时持有的Lease,保证后面其他客户端的写入能够正常申请到该文件的Lease。

NameNode内存数据结构非常丰富,这里对几个重要的数据结构进行了简单的描述,除了前面罗列之外,其实还有如SnapShotManager/CacheManager等,由于其内存占用有限且有一些特性还尚未稳定,这里不再展开。

四、问题

随着集群中数据规模的不断积累,NameNode内存占用随之成比例增长。不可避免的NameNode内存将逐渐成为集群发展的瓶颈,并开始暴漏诸多问题。

1、启动时间变长。NameNode的启动过程可以分成FsImage数据加载、editlogs回放、Checkpoint、DataNode的BlockReport几个阶段。数据规模较小时,启动时间可以控制在~10min以内,当元数据规模达到5亿(Namespace中INode数超过2亿,Block数接近3亿),FsImage文件大小将接近到20GB,加载FsImage数据就需要~14min,Checkpoint需要~6min,再加上其他阶段整个重启过程将持续~50min,极端情况甚至超过60min,虽然经过多轮优化重启过程已经能够稳定在~30min,但也非常耗时。如果数据规模继续增加,启动过程将同步增加。

2、性能开始下降。HDFS文件系统的所有元数据相关操作基本上均在NameNode端完成,当数据规模的增加致内存占用变大后,元数据的增删改查性能会出现下降,且这种下降趋势会因规模效应及复杂的处理逻辑被放大,相对复杂的RPC请求(如addblock)性能下降更加明显。

3、NameNode JVM FGC(Full GC)风险较高。主要体现在两个方面:(1)FGC频率增加;(2)FGC时间增加且风险不可控。针对NameNode的应用场景,目前看CMS内存回收算法比较主流,正常情况下,对超过100GB内存进行回收处理时,可以控制到秒级别的停顿时间,但是如果回收失败被降级到串行内存回收时,应用的停顿时间将达到数百秒,这对应用本身是致命的。

4、超大JVM Heap Size调试问题。如果线上集群性能表现变差,不得不通过分析内存才能得到结论时,会成为一件异常困难的事情。且不说Dump本身极其费时费力,Dump超大内存时存在极大概率使NameNode不可服务。

针对NameNode内存增长带来的诸多问题,社区和业界都在持续关注并尝试不同的解决方案。整体上两个思路:(1)扩展NameNode分散单点负载;(2)引入外部系统支持NameNode内存数据。

从2010年开始社区就投入大量精力持续解决,Federation方案[3]通过对NameNode进行水平扩展分散单点负载的方式解决NameNode的问题,经过几年的发展该方案逐渐稳定,目前已经被业界广泛使用。除此之外,社区也在尝试将Namespace存储值外部的KV存储系统如LevelDB[4],从而降低NameNode内存负载。

除社区外,业界也在尝试自己的解决方案。Baidu HDFS2[5]将元数据管理通过主从架构的集群形式提供服务,本质上是将原生NameNode管理的Namespace和BlockManagement进行物理拆分。其中Namespace负责管理整个文件系统的目录树及文件到BlockID集合的映射关系,BlockID到DataNode的映射关系是按照一定的规则分到多个服务节点分布式管理,这种方案与Lustre有相似之处(Hash-based Partition)。Taobao HDFS2[6]尝试过采用另外的思路,借助高速存储设备,将元数据通过外存设备进行持久化存储,保持NameNode完全无状态,实现NameNode无限扩展的可能。其他类似的诸多方案不一而足。

尽管社区和业界均对NameNode内存瓶颈有成熟的解决方案,但是不一定适用所有的场景,尤其是中小规模集群。结合实践过程和集群规模发展期可能遇到的NameNode内存相关问题这里有几点建议:

1、合并小文件。正如前面提到,目录/文件和Block均会占用NameNode内存空间,大量小文件会降低内存使用效率;另外,小文件的读写性能远远低于大文件的读写,主要原因对小文件读写需要在多个数据源切换,严重影响性能。

2、调整合适的BlockSize。主要针对集群内文件较大的业务场景,可以通过调整默认的Block Size大小(参数:dfs.blocksize,默认128M),降低NameNode的内存增长趋势。

3、HDFS Federation方案。当集群和数据均达到一定规模时,仅通过垂直扩展NameNode已不能很好的支持业务发展,可以考虑HDFS Federation方案实现对NameNode的水平扩展,在解决NameNode的内存问题的同时通过Federation可以达到良好的隔离性,不会因为单一应用压垮整集群。

五、总结

NameNode在整个HDFS系统架构中占据举足轻重的位置,内部数据和处理逻辑相对复杂,本文简单梳理了NameNode的内存全景及对其中几个关键数据结构,从NameNode内存核心数据视角对NameNode进行了简单的解读,并结合实际场景介绍了随着数据规模的增加NameNode内存可能遇到的问题及业界各种可借鉴的解决方案。

六、参考

[1] Apache Hadoop. https://hadoop.apache.org/.
[2] Apache Hadoop Source Code. https://github.com/apache/hadoop/tree/branch-2.4.1/.
[3] HDFS Federation. https://issues.apache.org/jira/browse/HDFS-1052.
[4] NemeNode Scalability. https://issues.apache.org/jira/browse/HDFS-5389.
[5] Baidu HDFS2. http://static.zhizuzhefu.com/wordpress_cp/uploads/2013/04/a9.pdf.
[6] Taobao HDFS2. https://github.com/taobao/ADFS.