一、背景
在 Hadoop 集群整个生命周期里,由于调整参数、Patch、升级等多种场景需要频繁操作 NameNode 重启,不论采用何种架构,重启期间集群整体存在可用性和可靠性的风险,所以优化 NameNode 重启非常关键。
本文基于 Hadoop-2.x 和 HA with QJM 社区架构和系统设计(如图 1 所示),通过梳理 NameNode 重启流程,并在此基础上,阐述对 NameNode 重启优化实践。
图 1 HDFS HA with QJM 架构图示
二、NameNode 重启流程
在 HDFS 的整个运行期里,所有元数据均在 NameNode 的内存集中管理,但是由于内存易失特性,一旦出现进程退出、宕机等异常情况,所有元数据都会丢失,给整个系统的数据安全会造成不可恢复的灾难。为了更好的容错能力,NameNode 会周期进行 Checkpoint,将其中的一部分元数据(文件系统的目录树 Namespace)刷到持久化设备上,即二进制文件 FSImage,这样的话即使 NameNode 出现异常也能从持久化设备上恢复元数据,保证了数据的安全可靠。
但是仅周期进行 Checkpoint 仍然无法保证所有数据的可靠,如前次 Checkpoint 之后写入的数据依然存在丢失的问题,所以将两次 Checkpoint 之间对 Namespace 写操作实时写入 EditLog 文件,通过这种方式可以保证 HDFS 元数据的绝对安全可靠。
事实上,除 Namespace 外,NameNode 还管理非常重要的元数据 BlocksMap,描述数据块 Block 与 DataNode 节点之间的对应关系。NameNode 并没有对这部分元数据同样操作持久化,原因是每个 DataNode 已经持有属于自己管理的 Block 集合,将所有 DataNode 的 Block 集合汇总后即可构造出完整 BlocksMap。
HA with QJM 架构下,NameNode 的整个重启过程中始终以 SBN(StandbyNameNode)角色完成。与前述流程对应,启动过程分以下几个阶段:
- 加载 FSImage;
- 回放 EditLog;
- 执行 Checkpoint;(非必须步骤,结合实际情况和参数确定,后续详述)
- 收集所有 DataNode 的注册和数据块汇报;
默认情况下,NameNode 会保存两个 FSImage 文件,与此对应,也会保存对应两次 Checkpoint 之后的所有 EditLog 文件。一般来说,NameNode 重启后,通过对 FSImage 文件名称判断,选择加载最新的 FSImage 文件及回放该 Checkpoint 之后生成的所有 EditLog,完成后根据加载的 EditLog 中操作条目数及距上次 Checkpoint 时间间隔(后续详述)确定是否需要执行 Checkpoint,之后进入等待所有 DataNode 注册和元数据汇报阶段,当这部分数据收集完成后,NameNode 的重启流程结束。
从线上 NameNode 历次重启时间数据看,各阶段耗时占比基本接近如图 2 所示。
图 2 NameNode 重启各阶段耗时占比
经过优化,在元数据总量 540M(目录树 240M,数据块 300M),超过 4K 规模的集群上重启 NameNode 总时间~35min,其中加载 FSImage 耗时~15min,秒级回放 EditLog,数据块汇报耗时~20min,基本能够满足生产环境的需求。
2.1 加载 FSImage
如前述,FSImage 文件记录了 HDFS 整个目录树 Namespace 相关的元数据。从 Hadoop-2.4.0 起,FSImage 开始采用 Google Protobuf 编码格式描述( HDFS-5698 ),详细描述文件见 fsimage.proto 。根据描述文件和实现逻辑,FSImage 文件格式如图 3 所示。
图 3 FSImage 文件格式
从 fsimage.proto 和 FSImage 文件存储格式容易看到,除了必要的文件头部校验(MAGIC)和尾部文件索引(FILESUMMARY)外,主要包含以下核心数据:
(0)NS_INFO(NameSystemSection):记录 HDFS 文件系统的全局信息,包括 NameSystem 的 ID,当前已经分配出去的最大 BlockID 以及 TransactionId 等信息;
(1)INODE(INodeSection):整个目录树所有节点数据,包括 INodeFile/INodeDirectory/INodeSymlink 等所有类型节点的属性数据,其中记录了如节点 id,节点名称,访问权限,创建和访问时间等等信息;
(2)INODE_DIR(INodeDirectorySection):整个目录树中所有节点之间的父子关系,配合 INODE 可构建完整的目录树;
(3)FILES_UNDERCONSTRUCTION(FilesUnderConstructionSection):尚未完成写入的文件集合,主要为重启时重建 Lease 集合;
(4)SNAPSHOT(SnapshotSection):记录 Snapshot 数据,快照是 Hadoop 2.1.0 引入的新特性,用于数据备份、回滚,以防止因用户误操作导致集群出现数据问题;
(5)SNAPSHOT_DIFF(SnapshotDiffSection):执行快照操作的目录 / 文件的 Diff 集合数据,与 SNAPSHOT 一起构建较完整的快照管理能力;
(6)INODE_REFERENCE(INodeReferenceSection):当目录 / 文件被操作处于快照,且该目录 / 文件被重命名后,会存在多条访问路径,INodeReference 就是为了解决该问题;
(7)SECRET_MANAGER(SecretManagerSection):记录 DelegationKey 和 DelegationToken 数据,根据 DelegationKey 及由 DelegationToken 构造出的 DelegationTokenIdentifier 方便进一步计算密码,以上数据可以完善所有合法 Token 集合;
(8)CACHE_MANAGER(CacheManagerSection):集中式缓存特性全局信息,集中式缓存特性是 Hadoop-2.3.0 为提升数据读性能引入的新特性;
(9)STRING_TABLE(StringTableSection):字符串到 id 的映射表,维护目录 / 文件的 Permission 字符到 ID 的映射,节省存储空间;
NameNode 执行 Checkpoint 时,遵循 Protobuf 定义及上述文件格式描述,重启加载 FSImage 时,同样按照 Protobuf 定义的格式从文件流中读出相应数据构建整个目录树 Namespace 及其他元数据。将 FSImage 文件从持久化设备加载到内存并构建出目录树结构后,实际上并没有完全恢复元数据到最新状态,因为每次 Checkpoint 之后还可能存在大量 HDFS 写操作。
2.2 回放 EditLog
NameNode 在响应客户端的写请求前,会首先更新内存相关元数据,然后再把这些操作记录在 EditLog 文件中,可以看到内存状态实际上要比 EditLog 数据更及时。
记录在 EditLog 之中的每个操作又称为一个事务,对应一个整数形式的事务编号。在当前实现中多个事务组成一个 Segment,生成独立的 EditLog 文件,其中文件名称标记了起止的事务编号,正在写入的 EditLog 文件仅标记起始事务编号。EditLog 文件的格式非常简单,没再通过 Google Protobuf 描述,文件格式如图 4 所示。
图 4 EditLog 文件格式
一个完整的 EditLog 文件包括四个部分内容,分别是:
(0)LAYOUTVERSION:版本信息;
(1)OP_START_LOG_SEGMENT:标识文件开始;
(2)RECORD:顺序逐个记录 HDFS 写操作的事务内容;
(3)OP_END_LOG_SEGMENT:标记文件结束;
NameNode 加载 FSImage 完成后,即开始对该 FSImage 文件之后(通过比较 FSImage 文件名称中包含的事务编号与 EditLog 文件名称的起始事务编号大小确定)生成的所有 EditLog 严格按照事务编号从小到大逐个遵循上述的格式进行每一个 HDFS 写操作事务回放。
NameNode 加载完所有必需的 EditLog 文件数据后,内存中的目录树即恢复到了最新状态。
2.3 DataNode 注册汇报
经过前面两个步骤,主要的元数据被构建,HDFS 的整个目录树被完整建立,但是并没有掌握从数据块 Block 与 DataNode 之间的对应关系 BlocksMap,甚至对 DataNode 的情况都不掌握,所以需要等待 DataNode 注册,并完成对从 DataNode 汇报上来的数据块汇总。待汇总的数据量达到预设比例(dfs.namenode.safemode.threshold-pct)后退出 Safemode。
NameNode 重启经过加载 FSImage 和回放 EditLog 后,所有 DataNode 不管进程是否发生过重启,都必须经过以下两个步骤:
(0)DataNode 重新注册 RegisterDataNode;
(1)DataNode 汇报所有数据块 BlockReport;
对于节点规模较大和元数据量较大的集群,这个阶段的耗时会非常可观。主要有三点原因:
(0)处理 BlockReport 的逻辑比较复杂,相对其他 RPC 操作耗时较长。图 5 对比了 BlockReport 和 AddBlock 两种不同 RPC 的处理时间,尽管 AddBlock 操作也相对复杂,但是对比来看,BlockReport 的处理时间显著高于 AddBlock 处理时间;
(1)NameNode 对每一个 BlockReport 的 RPC 请求处理都需要持有全局锁,也就是说对于 BlockReport 类型 RPC 请求实际上是串行处理;
(2)NameNode 重启时所有 DataNode 集中在同一时间段进行 BlockReport 请求;
(点击放大图像)
图5 BlockReport 和AddBlock 两个RPC 处理时间对比
前文 NameNode 内存全景中详细描述过 Block 在 NameNode 元数据中的关键作用及与 Namespace/DataNode/BlocksMap 的复杂关系,从中也可以看出,每个新增 Block 需要维护多个关系,更何况重启过程中所有 Block 都需要建立同样复杂关系,所以耗时相对较高。
三、重启优化
根据前面对 NameNode 重启过程的简单梳理,在各个阶段可以适当的实施优化以加快 NameNode 重启过程。
0、 HDFS-7097 解决重启过程中 SBN 执行 Checkpoint 时不能处理 BlockReport 请求的问题;
Fix: 2.7.0
Hadoop-2.7.0 版本前,SBN(StandbyNameNode)在执行 Checkpoint 操作前会先获得全局读写锁 fsLock,在此期间,BlockReport 请求由于不能获得全局写锁会持续处于等待状态,直到 Checkpoint 完成后释放了 fsLock 锁后才能继续。NameNode 重启的第三个阶段,同样存在这种情况。而且对于规模较大的集群,每次 Checkpoint 时间在分钟级别,对整个重启过程影响非常大。实际上,Checkpoint 是对目录树的持久化操作,并不涉及 BlocksMap 数据结构,所以 Checkpoint 期间是可以让 BlockReport 请求直接通过,这样可以节省期间 BlockReport 排队等待带来的时间开销, HDFS-7097 正是将锁粒度放小解决了 Checkpoint 过程不能处理 BlockReport 类型 RPC 请求的问题。
与 HDFS-7097 相对,另一种思路也值得借鉴,就是重启过程尽可能避免出现 Checkpoint。触发 Checkpoint 有两种情况:时间周期或 HDFS 写操作事务数,分别通过参数 dfs.namenode.checkpoint.period 和 dfs.namenode.checkpoint.txns 控制,默认值分别是 3600s 和 1,000,000,即默认情况下一个小时或者写操作的事务数超过 1,000,000 触发一次 Checkpoint。为了避免在重启过程中频繁执行 Checkpoint,可以适当调大 dfs.namenode.checkpoint.txns,建议值 10,000,000 ~ 20,000,000,带来的影响是 EditLog 文件累计的个数会稍有增加。从实践经验上看,对一个有亿级别元数据量的 NameNode,回放一个 EditLog 文件(默认 1,000,000 写操作事务)时间在秒级,但是执行一次 Checkpoint 时间通常在分钟级别,综合权衡减少 Checkpoint 次数和增加 EditLog 文件数收益比较明显。
1、 HDFS-6763 解决 SBN 每间隔 1min 全局计算和验证 Quota 值导致进程 Hang 住数秒的问题;
Fix: 2.8.0
ANN(ActiveNameNode)将 HDFS 写操作实时写入 JN 的 EditLog 文件,为同步数据,SBN 默认间隔 1min 从 JN 拉取一次 EditLog 文件并进行回放,完成后执行全局 Quota 检查和计算,当 Namespace 规模变大后,全局计算和检查 Quota 会非常耗时,在此期间,整个 SBN 的 Namenode 进程会被 Hang 住,以至于包括 DN 心跳和 BlockReport 在内的所有 RPC 请求都不能及时处理。NameNode 重启过程中这个问题影响突出。
实际上,SBN 在 EditLog Tailer 阶段计算和检查 Quota 完全没有必要,HDFS-6763 将这段处理逻辑后移到主从切换时进行,解决 SBN 进程间隔 1min 被 Hang 住的问题。
从优化效果上看,对一个拥有接近五亿元数据量,其中两亿数据块的 NameNode,优化前数据块汇报阶段耗时~30min,其中触发超过 20 次由于计算和检查 Quota 导致进程 Hang 住~20s 的情况,整个 BlockReport 阶段存在超过 5min 无效时间开销,优化后可到~25min。
2、 HDFS-7980 简化首次 BlockReport 处理逻辑优化重启时间;
Fix: 2.7.1
NameNode 加载完元数据后,所有 DataNode 尝试开始进行数据块汇报,如果汇报的数据块相关元数据还没有加载,先暂存消息队列,当 NameNode 完成加载相关元数据后,再处理该消息队列。对第一次块汇报的处理比较特别(NameNode 重启后,所有 DataNode 的 BlockReport 都会被标记成首次数据块汇报),为提高处理速度,仅验证块是否损坏,之后判断块状态是否为 FINALIZED,若是建立数据块与 DataNode 的映射关系,建立与目录树中文件的关联关系,其他信息一概暂不处理。对于非初次数据块汇报,处理逻辑要复杂很多,对报告的每个数据块,不仅检查是否损坏,是否为 FINALIZED 状态,还会检查是否无效,是否需要删除,是否为 UC 状态等等;验证通过后建立数据块与 DataNode 的映射关系,建立与目录树中文件的关联关系。
初次数据块汇报的处理逻辑独立出来,主要原因有两方面:
(0)加快 NameNode 的启动时间;测试数据显示含~500M 元数据的 NameNode 在处理 800K 个数据块的初次块汇报的处理时间比正常块汇报的处理时间可降低一个数量级;
(1)启动过程中,不提供正常读写服务,所以只要确保正常数据(整个 Namespace 和所有 FINALIZED 状态 Blocks)无误,无效和冗余数据处理完全可以延后到 IBR(IncrementalBlockReport)或下次 BR(BlockReport);
这本来是非常合理和正常的设计逻辑,但是实现时 NameNode 在判断是否为首次数据块块汇报的逻辑一直存在问题,导致这段非常好的改进点逻辑实际上长期并未真正执行到,直到 HDFS-7980 在 Hadoop-2.7.1 修复该问题。 HDFS-7980 的优化效果非常明显,测试显示,对含 80K Blocks 的 BlockReport RPC 请求的处理时间从~500ms 可优化到~100ms,从重启期整个 BlockReport 阶段看,在超过 600M 元数据,其中 300M 数据块的 NameNode 显示该阶段从~50min 优化到~25min。
3、 HDFS-7503 解决重启前大删除操作会造成重启后锁内写日志降低处理能力;
Fix: 2.7.0
若 NameNode 重启前产生过大删除操作,当 NameNode 加载完 FSImage 并回放了所有 EditLog 构建起最新目录树结构后,在处理 DataNode 的 BlockReport 时,会发现有大量 Block 不属于任何文件,Hadoop-2.7.0 版本前,对于这类情况的输出日志逻辑在全局锁内,由于存在大量 IO 操作的耗时,会严重拉长处理 BlockReport 的处理时间,影响 NameNode 重启时间。 HDFS-7503 的解决办法非常简单,把日志输出逻辑移出全局锁外。线上效果上看对同类场景优化比较明显,不过如果重启前不触发大的删除操作影响不大。
4、防止热备节点 SBN(StandbyNameNode)/ 冷备节点 SNN(SecondaryNameNode)长时间未正常运行堆积大量 Editlog 拖慢 NameNode 重启时间;
不论选择 HA 热备方案 SBN(StandbyNameNode)还是冷备方案 SNN(SecondaryNameNode)架构,执行 Checkpoint 的逻辑几乎一致,如图 6 所示。如果 SBN/SNN 服务长时间未正常运行,Checkpoint 不能按照预期执行,这样会积压大量 EditLog。积压的 EditLog 文件越多,重启 NameNode 需要加载 EditLog 时间越长。所以尽可能避免出现 SNN/SBN 长时间未正常服务的状态。
图 6 Checkpoint 流程
在一个有 500M 元数据的 NameNode 上测试加载一个 200K 次 HDFS 事务操作的 EditLog 文件耗时~5s,按照默认 2min 的 EditLog 滚动周期,如果一周时间 SBN/SNN 未能正常工作,则会累积~5K 个 EditLog 文件,此后一旦发生 NameNode 重启,仅加载 EditLog 文件的时间就需要~7h,也就是整个集群存在超过 7h 不可用风险,所以切记要保证 SBN/SNN 不能长时间故障。
5、 HDFS-6425 HDFS-6772 NameNode 重启后 DataNode 快速退出 blockContentsStale 状态防止 PostponedMisreplicatedBlocks 过大影响对其他 RPC 请求的处理能力;
当集群中大量数据块的实际存储副本个数超过副本数时(跨机房架构下这种情况比较常见),NameNode 重启后会迅速填充到 PostponedMisreplicatedBlocks,直到相关数据块所在的所有 DataNode 汇报完成且退出 Stale 状态后才能被清理。如果 PostponedMisreplicatedBlocks 数据量较大,每次全遍历需要消耗大量时间,且整个过程也要持有全局锁,严重影响处理 BlockReport 的性能, HDFS-6425 和 HDFS-6772 分别将可能在 BlockReport 逻辑内部遍历非常大的数据结构 PostponedMisreplicatedBlocks 优化到异步执行,并在 NameNode 重启后让 DataNode 快速退出 blockContentsStale 状态避免 PostponedMisreplicatedBlocks 过大入手优化重启效率。
6、降低 BlockReport 时数据规模;
NameNode 处理 BlockReport 的效率低主要原因还是每次 BlockReport 所带的 Block 规模过大造成,所以可以通过调整 Block 数量阈值,将一次 BlockReport 分成多盘分别汇报,以提高 NameNode 对 BlockReport 的处理效率。可参考的参数为:dfs.blockreport.split.threshold,默认值 1,000,000,即当 DataNode 本地的 Block 个数超过 1,000,000 时才会分盘进行汇报,建议将该参数适当调小,具体数值可结合 NameNode 的处理 BlockReport 时间及集群中所有 DataNode 管理的 Block 量分布确定。
7、重启完成后对比检查数据块上报情况;
前面提到 NameNode 汇总 DataNode 上报的数据块量达到预设比例(dfs.namenode.safemode.threshold-pct)后就会退出 Safemode,一般情况下,当 NameNode 退出 Safemode 后,我们认为已经具备提供正常服务的条件。但是对规模较大的集群,按照这种默认策略及时执行主从切换后,容易出现短时间丢块的问题。考虑在 200M 数据块的集群,默认配置项 dfs.namenode.safemode.threshold-pct=0.999,也就是当 NameNode 收集到 200M*0.999=199.8M 数据块后即可退出 Safemode,此时实际上还有 200K 数据块没有上报,如果强行执行主从切换,会出现大量的丢块问题,直到数据块汇报完成。应对的办法比较简单,尝试调大 dfs.namenode.safemode.threshold-pct 到 1,这样只有所有数据块上报后才会退出 Safemode。但是这种办法一样不能保证万无一失,如果启动过程中有 DataNode 汇报完数据块后进程挂掉,同样存在短时间丢失数据的问题,因为 NameNode 汇总上报数据块时并不检查副本数,所以更稳妥的解决办法是利用主从 NameNode 的 JMX 数据对比所有 DataNode 当前汇报数据块量的差异,当差异都较小后再执行主从切换可以保证不发生上述问题。
8、其他;
除了优化 NameNode 重启时间,实际运维中还会遇到需要滚动重启集群所有节点或者一次性重启整集群的情况,不恰当的重启方式也会严重影响服务的恢复时间,所以合理控制重启的节奏或选择合适的重启方式尤为关键, HDFS 集群启动方式分析一文对集群重启方式进行了详细的阐述,这里就不再展开。
经过多次优化调整,从线上 NameNode 历次的重启时间监控指标上看,收益非常明显,图 7 截取了其中几次 NameNode 重启时元数据量及重启时间开销对比,图中直观显示在 500M 元数据量级下,重启时间从~4000s 优化到~2000s。
图 7 NameNode 重启时间对比
这里罗列了一小部分实践过程中可以有效优化重启 NameNode 时间或者重启全集群的点,其中包括了社区成熟 Patch 和相关参数优化,虽然实现逻辑都很小,但是实践收益非常明显。当然除了上述提到,NameNode 重启还有很多可以优化的地方,比如优化 FSImage 格式,并行加载等等,社区也在持续关注和优化,部分讨论的思路也值得关注、借鉴和参考。
四、总结
NameNode 重启甚至全集群重启在整个 Hadoop 集群的生命周期内是比较频繁的运维操作,优化重启时间可以极大提升运维效率,避免可能存在的风险。本文通过分析 NameNode 启动流程,并结合实践过程简单罗列了几个供参考的有效优化点,借此希望能给实践过程提供可优化的方向和思路。
五、参考
感谢丁晓昀对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论