深入HDFS——HA和QJM
引入
通过前面篇章,我们对于HDFS的设计与运行已经有了很不错的理解,但是在企业生产中,我们还需要关注到系统的稳定性。
我们知道HDFS参考GFS的很多设计思路,比如也是采用的主从架构设计,这样让整个架构变得非常简单,并且能避免需要管理复杂一致性的问题。
但也正因此,架构设计中的这个主节点(也就是HDFS中的NameNode),会变成集群的“热点”。一旦它出现故障,整个集群都没办法提供服务了,这样系统的稳定性肯定是不符合企业生产预期的。
为了解决这些高可用保障的问题,在Hadoop 2.0 之后,HDFS推出了HA功能,并且在Hadoop 3.0 之后,又推出了多个NameNode的HA功能。
而HDFS提供的HA功能,实际是基于两种不同共享数据的存储介质的NameNode的HA方案:
- 一种是基于QJM(Quorum Journal Manager)
- 一种是基于NFS(Network File System)
在实际生产环境中,由于NFS 的性能等问题,通常不会考虑使用,所以本文主要讲解基于QJM的HA。
需求梳理
关于HA的功能,肯定是需要能快速顶替问题主节点的备用节点,关于这个备用节点相关的设计,我们还是老样子,梳理一下核心需求:
- 要能够有数据共享能力,保障主备节点的数据一致;
- 要能够快速且正确的剔除问题主节点,让正常的备用节点无缝切换。
下面就看下针对这两个核心需求,HDFS是如何设计实现的。
数据共享
在HDFS里面,会把当前正在提供服务的主节点称为Active NameNode(ANN),而备用节点就叫做StandbyNameNode(SNN)。
我们知道,在NameNode中存储了HDFS中所有元数据信息,在HA设计中,当ANN挂掉后,期望的是SNN要及时顶上,这就需要将所有的元数据同步到SNN节点。
那我们来看看怎么实现好:
- 在向HDFS中写入一个文件时,让元数据同步写入ANN和SNN。这样会导致当SNN挂掉时,影响到ANN,肯定是不行的。
- 让元数据异步写入ANN和SNN。但这样有会有概率ANN挂掉的时候,元数据又没有及时异步写入到SNN,那切换后也会导致数据丢失。这个方案也不行。
- 引入第三方存储。就是每次向HDFS中写入文件时,需要将Editslog同步写入这个第三方共享存储,这个步骤成功才能认定写文件成功,然后SNN定期从共享存储中同步Editslog,以便拥有完整元数据,确保ANN挂掉切换后数据也不丢失。
HDFS自然时采用的第三种方案,这个第三方存储,在HA方案中叫做“共享存储”。
HDFS将Cloudera公司实现的QJM(Quorum Journal Manager)方案作为默认的共享存储实现。
在QJM方案中注意如下几点:
基于QJM的共享存储系统主要用于保存Editslog,并不保存FSImage文件,FSImage文件还是在NameNode本地磁盘中。
QJM共享存储采用多个称为JournalNode的节点组成的JournalNode集群来存储EditsLog。每个JournalNode保存同样的EditsLog副本。
每次NameNode写EditsLog时,除了向本地磁盘写入EditsLog外,也会并行的向JournalNode集群中每个JournalNode发送写请求,只要大多数的JournalNode节点返回成功就认为向JournalNode集群中写入EditsLog成功。
如果有2N+1台JournalNode,那么根据大多数的原则,最多可以容忍有N台JournalNode节点挂掉。
架构设计
HDFS HA 实现设计架构图如下:
当客户端操作HDFS集群时,Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog。
当处于 Standby 状态的 NameNode 转换为 Active 状态的时候,有可能上一个 Active NameNode 发生了异常退出,那么 JournalNode 集群中各个 JournalNode 上的 EditLog 就可能会处于不一致的状态,所以首先要做的事情就是让 JournalNode 集群中各个节点上的 EditLog 恢复为一致,然后Standby NameNode会从JournalNode集群中同步EditsLog,然后对外提供服务。
注意:在NameNode HA中不再需要SecondaryNameNode角色,该角色被StandbyNameNode替代。
通过Journal Node实现NameNode HA时,可以手动将Standby NameNode切换成Active NameNode,也可以通过自动方式实现NameNode切换。
快速正确切换
但是通过手动进行切换StandbyNamenode为Active NameNode,效率太低了,不满足我们快速的需求。
所以引入了Zookeeper进行协调。通过Zookeeper来检测Activate NameNode节点是否挂掉,如果挂掉立即将Standby NameNode切换成Active NameNode,自动实现NameNode HA。
架构设计
结合Zookeeper后的架构设计图如下:
以上各个角色解释如下:
-
AcitveNameNode:主 NameNode,只有主NameNode才能对外提供读写服务。
-
Secondby NameNode:备用NameNode,定时同步Journal集群中的editslog元数据。
-
ZKFailoverController:ZKFailoverController 作为独立的进程运行,对 NameNode 的主备切换进行总体控制。ZKFailoverController 能及时检测到 NameNode 的健康状况,在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换。
-
Zookeeper集群:分布式协调器,NameNode选主使用。
-
Journal集群:Journal集群作为共享存储系统保存HDFS运行过程中的元数据,ANN和SNN通过Journal集群实现元数据同步。
-
DataNode节点:除了通过共享存储系统共享 HDFS 的元数据信息之外,主 NameNode 和备 NameNode 还需要共享 HDFS 的数据块和 DataNode 之间的映射关系。DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。
切换流程
NameNode 主备切换主要由 ZKFailoverController、HealthMonitor 和 ActiveStandbyElector 这 3 个组件来协同实现:
-
ZKFailoverController 作为 NameNode 机器上一个独立的进程启动 (在 hdfs 集群中进程名为 zkfc),启动的时候会创建 HealthMonitor 和 ActiveStandbyElector 这两个主要的内部组件,ZKFailoverController 在创建 HealthMonitor 和 ActiveStandbyElector 的同时,也会向 HealthMonitor 和 ActiveStandbyElector 注册相应的回调方法。
-
HealthMonitor 主要负责检测 NameNode 的健康状态,如果检测到 NameNode 的状态发生变化,会回调 ZKFailoverController 的相应方法进行自动的主备选举。
-
ActiveStandbyElector 主要负责完成自动的主备选举,内部封装了 Zookeeper 的处理逻辑,一旦 Zookeeper 主备选举完成,会回调 ZKFailoverController 的相应方法来进行 NameNode 的主备状态切换。
NameNode主备切换流程如下:
-
HealthMonitor 初始化完成之后会启动内部的线程来定时调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法,对 NameNode 的健康状态进行检测。
-
HealthMonitor 如果检测到 NameNode 的健康状态发生变化,会回调 ZKFailoverController 注册的相应方法进行处理。
-
如果 ZKFailoverController 判断需要进行主备切换,会首先使用 ActiveStandbyElector 来进行自动的主备选举。
-
ActiveStandbyElector 与 Zookeeper 进行交互完成自动的主备选举。
-
ActiveStandbyElector 在主备选举完成后,会回调 ZKFailoverController 的相应方法来通知当前的 NameNode 成为主 NameNode 或备 NameNode。
-
ZKFailoverController 调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法将 NameNode 转换为 Active 状态或 Standby 状态。
脑裂问题
细心的小伙伴会注意到,我在标题强调了正确,这是因为可能会出现“脑裂"的情况。
当网络抖动时,ZKFC检测不到Active NameNode,此时认为NameNode挂掉了,因此将Standby NameNode切换成Active NameNode,而旧的Active NameNode由于网络抖动,接收不到zkfc的切换命令,此时两个NameNode都是Active状态,这就是脑裂问题。
解决方案
那么HDFS HA中如何防止脑裂问题的呢?
HDFS集群初始启动时,Namenode的主备选举是通过 ActiveStandbyElector 来完成的,ActiveStandbyElector 主要是利用了 Zookeeper 的写一致性和临时节点机制。
具体流程
具体的主备选举实现如下:
1.创建锁节点
如果 HealthMonitor 检测到对应的 NameNode 的状态正常,那么表示这个 NameNode 有资格参加 Zookeeper 的主备选举。
如果目前还没有进行过主备选举的话,那么相应的 ActiveStandbyElector 就会发起一次主备选举,尝试在 Zookeeper 上创建一个路径为/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 的临时节点,Zookeeper 的写一致性会保证最终只会有一个 ActiveStandbyElector 创建成功。
- 创建成功的 ActiveStandbyElector 对应的 NameNode 就会成为主 NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的方法进一步将对应的 NameNode 切换为 Active 状态。
- 而创建失败的 ActiveStandbyElector 对应的NameNode成为备用NameNode,ActiveStandbyElector 会回调 ZKFailoverController 的方法进一步将对应的 NameNode 切换为 Standby 状态。
${dfs.nameservices} 为 Hadoop 的配置参数 dfs.nameservices 的值
2.注册 Watcher 监听
不管创建 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点是否成功,ActiveStandbyElector 随后都会向 Zookeeper 注册一个 Watcher ,来监听这个节点的状态变化事件,ActiveStandbyElector 主要关注这个节点的 NodeDeleted 事件。
3.自动触发主备选举
如果 Active NameNode 对应的 HealthMonitor 检测到 NameNode 的状态异常时, ZKFailoverController 会主动删除当前在 Zookeeper 上建立的临时节点 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock。
- 这样处于 Standby 状态的 NameNode 的 ActiveStandbyElector 注册的监听器就会收到这个节点的 NodeDeleted 事件。收到这个事件之后,会马上再次进入到创建 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点的流程,如果创建成功,这个本来处于 Standby 状态的 NameNode 就选举为主 NameNode 并随后开始切换为 Active 状态。
- 当然,如果是 Active 状态的 NameNode 所在的机器整个宕掉的话,那么根据 Zookeeper 的临时节点特性,/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点会自动被删除,从而也会自动进行一次主备切换。
以上过程中,Standby NameNode成功创建 Zookeeper 节点 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 成为Active NameNode之后,还会创建另外一个路径为 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 的持久节点,这个节点里面保存了这个 Active NameNode 的地址信息。Active NameNode 的ActiveStandbyElector 在正常的状态下关闭 Zookeeper Session 的时候 (注意由于/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 是临时节点,也会随之删除)会一起删除节点 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb。
但是如果 ActiveStandbyElector 在异常的状态下 Zookeeper Session 关闭(比如 Zookeeper 假死),那么由于/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 是持久节点,会一直保留下来。
后面当另一个 NameNode 选主成功之后,会注意到上一个 Active NameNode 遗留下来的这个节点,从而会回调 ZKFailoverController 的方法对旧的 Active NameNode 进行隔离(fencing)操作以避免出现脑裂问题,fencing 操作会通过SSH将旧的Active NameNode进程尝试转换成Standby状态,如果不能转换成Standby状态就直接将对应进程杀死。
总结
今天带大家梳理了HDFS是如何通过引入QJM和ZK来提高系统的高可用,保障系统稳定性的。
后面随着深入大数据领域,我们会发现,很多大数据技术组件都是采用的主从架构,那自然都避免不了单点故障的问题,要如何提高系统高可用,保障稳定性,今天的很多思路都是可以迁移的。