HDFS 集群重启过程详解
除 Namespace 外,NameNode 还管理非常重要的元数据BlocksMap,描述数据块 Block 与 datanode 节点之间的对应关系。NameNode 并没有对这部分元数据同样操作持久化,原因是每个 datanode
已经持有属于自己管理的 Block 集合,将所有 datanode 的 Block 集合汇总后即可构造出完整 BlocksMap
在高可用状态下,NameNode 的整个重启过程中始终以 StandbyNameNode 角色完成,启动过程分以下几个阶段:
1.加载 FSImage;
2.回放 EditLog;
3.执行 CheckPoint;
4.收集所有 datanode 的注册和数据块汇报。
默认情况下,NameNode 会保存两个 FSImage 文件,与此对应,也会保存对应两次 Checkpoint 之后的所有 EditLog 文件。
一般来说,NameNode 重启后,通过对 FSImage 文件名称判断,选择加载最新的FSImage 文件及回放该 Checkpoint 之后生成的所有 EditLog,完成后根据加载的 EditLog 中操作条目数及距上次 Checkpoint 时间间隔(后续详述)确定是否需要执行 Checkpoint,之后进入等待所有 datanode 注册和元数据汇报阶段,当这部分数据收集完成后,NameNode 的重启流程结束。
NameNode 重启各阶段耗时占比:
1. datanode 注册汇报
NameNode 重 启 经 过 加 载 FSImage 和 回 放 EditLog 后 , 所 有datanode 不管进程是否发生过重启,都必须经过以下两个步骤:
(1)datanode 重新注册 Registerdatanode;
(2)datanode 汇报所有数据块 BlockReport;
对于节点规模较大和元数据量较大的集群,这个阶段的耗时会比较长,主要有三点原因:
(1)处理 BlockReport 的逻辑比较复杂,相对其他 RPC 操作耗时较长,尽管 AddBlock 操作也相对复杂,但是对比来看,BlockReport 的处理时间显著高于 AddBlock 处理时间;
(2)NameNode 对每一个 BlockReport 的 RPC 请求处理都需要持有全局锁,也就是说对于 BlockReport 类型 RPC 请求实际上是串行处理;
( 3 ) NameNode 重 启 时 所 有 datanode 集 中 在 同 一 时 间 段 进 行BlockReport 请求;
HDFS重启优化
1. HDFS-7097 解决重启过程中 StandbyNameNode 执行 Checkpoint时不能处理 BlockReport 请求的问题;
Hadoop-2.7.0 版本前,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 文件数收益比较明显。
2.HDFS-6763 解决 StandbyNameNode 每间隔 1min 全局计算和验证Quota 值导致进程 Hang 住数秒的问题
ANN(ActiveNameNode)将 HDFS 写操作实时写入 JN 的 EditLog 文件,
为 同 步 数 据 , StandbyNameNode 默 认 间 隔 1min 从 JN 拉 取 一 次EditLog 文件 并进行 回放, 完成 后执行 全局 Quota 检查 和计算 ,当Namespace 规模变大后,全局计算和检查 Quota 会非常耗时,在此期间,整个 StandbyNameNode 的 Namenode 进程会被 Hang 住,以至于包括DN 心 跳 和 BlockReport 在 内 的 所 有 RPC 请 求 都 不 能 及 时 处 理 。NameNode 重启过程中这个问题影响突出。
实际上,StandbyNameNode 在 EditLog Tailer 阶段计算和检查 Quota 完全没有必要,HDFS-6763 将这段处理逻辑后移到主从切换时进行,解决StandbyNameNode 进程间隔 1min 被 Hang 住的问题。
3.HDFS-7980 简化首次 BlockReport 处理逻辑优化重启时间;
NameNode 加载完元数据后,所有 datanode 尝试开始进行数据块汇报,如果汇报的数据块相关元数据还没有加载,先暂存消息队列,当NameNode 完成加载相关元数据后,再处理该消息队列。对第一次块汇报的处理比较特别(NameNode 重启后,所有 datanode 的 BlockReport 都会被标记成首次数据块汇报),为提高处理速度,仅验证块是否损坏,之后判断块状态是否为 FINALIZED,若是建立数据块与 datanode 的映射关系,建立与目录树中文件的关联关系,其他信息一概暂不处理。对于非初次数据块汇报,处理逻辑要复杂很多,对报告的每个数据块,不仅检查是否损坏,是否为FINALIZED 状态,还会检查是否无效,是否需要删除,是否为 UC 状态等等;验证通过后建立数据块与 datanode 的映射关系,建立与目录树中文件的关联关系。
初次数据块汇报的处理逻辑独立出来,主要原因有两方面:
(1)加快 NameNode 的启动时间;测试数据显示含~500M 元数据的NameNode 在处理 800K 个数据块的初次块汇报的处理时间比正常块汇报的处理时间可降低一个数量级;
(2)启动过程中,不提供正常读写服务,所以只要确保正常数据(整个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。
4.防止热备节点 StandbyNameNode 长时间未正常运行堆积大量 Editlog拖慢 NameNode 重启时间;
如果 StandbyNameNode 服务长时间未正常运行,Checkpoint 不能按照预期执行,这样会积压大量 EditLog。积压的 EditLog 文件越多,重启NameNode 需 要 加 载 EditLog 时 间 越 长 。 所 以 尽 可 能 避 免 出 现SNN/StandbyNameNode 长时间未正常服务的状态。
5.降低 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 量分布确定,我们产线环境配置的是 10W。
3.深度分析
对于 BlockReport 类型的 RPC 请求,重启全集群 datanode 与重启NameNode,RPC 处理时间有一个数量级的差别。这种差别通过代码得到验证。
可以看到 NameNode 对 BlockReport 的处理方式仅区别于是否为初次BlockReport。初次 BlockReport 显然只发生在 NameNode 重启期间。processFiRegionServertBlockReport:对 Standby 节点(NameNode重启期间均为 Standby),如果汇报的数据块相关元数据还没有加载,会将报告的块信息暂存队列,当 Standby 节点完成加载相关元数据后,再处理该消息队列;对第一次块汇报的处理比较特别,为提高处理效率,仅验证块是否损坏,建立块与 DN 节点的映射,其他信息一概暂不处理。
processReport:对于非初次块汇报,处理逻辑要复杂很多;对报告的每个块信息,不仅会建立块与 DN 的映射,还会检查是否损坏,是否无效,是否需要删除,是否为 UnderConstruction 状态(指的是一个 block 块处于正在被写入的状态状态)等等。
初次块汇报的处理逻辑单独拿出来,主要原因有两方面:
1、加快 NameNode 的启动时间;统计数据也能说明,初次块汇报的处理时间比正常块汇报的处理时间能节省约一个数量级的时间。
2、由于启动过程中,不提供正常读写服务,所以只要确保正常数据(整个Namespace 和所有 FINALIZED 状态 Blocks)无误,无效和冗余数据处理完全可以延后。
说明:是否选择 processFiRegionServertBlockReport 处理逻辑不会因为NameNode 当前为 safemode 或者 standby 发生变化,仅 NameNode 重启生效;BlockReport 的处理时间与 datanode 数据规模正相关,如果不操作
NameNode 重启,BlockReport 处理时间会因为处理逻辑复杂带来额外的处理时间,约一个数量级的差别。
NameNode 对非第一次 BlockReport 的复杂处理逻辑只是 NameNode负载持续处于高位的诱因,在其诱发下发生了一系列“滚雪球”式的异常放大。
1、所有 datanode 进程被关闭后,NameNode 的 CallQueue(默认大小:3200)会被快速消费完;
2、所有 datanode 进程被重启后,NameNode 的 CallQueue 会被迅速填充,主要来自 datanode 重启后正常流程里的 VeRegionServerionRequest 和registerdatanode 两类 RPC 请求,由于均较轻量,所以也会被迅速消费完;
3、之后 datanode 进入 BlockReport 流程,NameNode 的 CallQueue 填充内容开始从 VeRegionServerionRequest 和 registerdatanode 向BlockReport 过渡;
直到 CallQueue 里几乎被所有 BlockReport 填充满。
重启 NameNode 后会发生完全不同的情况。
1、NameNode 重启后,首先加载 FsImage,此时,除 Namespace 外NameNode 的元数据几乎为空,此后开始接收 datanode 过来的 RPC 请求(绝大多数为 Heartbeat);
2、NameNode 接收到 Heartbeat 后由于在初始状态会要求 datanode 重新注册;由于 Heartbeat 间隔是 3s,所以从 NameNode 的角度看,所有datanode 的后续一系列 RPC 请求会被散列到 3s 时间线上;
3、datanode 向 NameNode 注册完成后立即开始 BlockReport;由于上步中提到的 3s 时间线散列关系,队列里后半部分 BlockReport 请求和VeRegionServerionRequest/registerdatanode 请求会出现相互交叉的情况;
4、处理 BlockReport 时部分 RPC 请求一样会发生超时;
5、由于超时重试,所以部分 BlockReport 和 registerdatanode 需要重试;可以发现不同于重启所有 datanode 时重试的 RPC 几乎都是 BlockReport,这里重试的 RPC 包括了VeRegionServerionRequest/registerdatanode(可以从日志证实),这就大幅降低了 NameNode 的负载,避免了“滚雪球”式高负载 RPC 堆积,使异常有效收敛。
避免重启大量 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;
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、重启 10 以内 datanode 观察会不会对集群造成雪崩式灾难,但是可能出现短时间内服务不可用状态,可用调小同时每批次的重启个数。
3、全集群升级时,建议 NameNode 和 datanode 均重启,在预期时间内可恢复服务。
欢迎来撩 : 汇总all