通过联合使用在多个文件系统中备份 NameNode 的元数据和通过备用 NameNode 创建监测点能防止数据丢失,但是依旧无法实现文件系统的高可用性。NameNode依旧存在单点失效(SPOF)的问题。如果 NameNode 失效了,那么所有的客户端,包括MapReduce作业,均无法读、写或列举文件,因为NameNode是唯一存储元数据与文件到数据块映射的地方,对于一个大型并拥有大量文件和数据块的集群,NameNode 的冷启动需要30分钟,甚至更长时间,系统恢复时间太长了,也会影响到日常维护。在这一情况下,Hadoop系统无法提供服务直到有新的 NameNode 上线。
在这样的情况下要向从一个失效的 NameNode 恢复,系统管理员得启动一个拥有文件系统元数据副本得新的NameNode,并配置DataNode和客户端以便使用这个新的 NameNode。新的 NameNode 直到满足以下情形才能相应服务:
Hadoop 2.X 以上版本针对上述问题增加了对 HDFS 高可用性(HA)的支持。在这一实现中,配置了一对活动-备用(active-standby) NameNode。当活动NameNode失效,备用 NameNode 就会接管它的任务并开始服务于来自客户端的请求,不会有任何明显中断。实现这一目标需要在架构上做如下修改。HDFS HA架构图如下所示:
有两种高可用性共享存储可以做出选择:NFS 过滤器或群体日志管理器(QJM, quorum journal manager)。QJM是一个专用的HDFS实现,为提供一个高可用的编辑日志而设计,被推荐用于大多数HDFS部署中,同时,QJM 的实现并没使用 Zookeeper,但在HDFS HA选取活动的NameNode时使用了Zookeeper技术。QJM以一组日志节点(journalnode)的形式运行,一般是奇数点结点组成,每个 JournalNode 对外有一个简易的RPC接口,以供NameNode读写EditLog到JN本地磁盘。当写EditLog时,NameNode会同时向所有JournalNode并行写文件,只要有 N/2+1 结点写成功则认为此次写操作成功,遵循Paxos协议。其内部实现框架如下:
从图中可看出,主要是涉及 EditLog 的不同管理对象和输出流对象,每种对象发挥着各自不同作用:
上面提到EditLog,NameNode会把EditLog同时写到本地和JournalNode。写本地由配置中参数dfs.namenode.name.dir控制,写JN由参数dfs.namenode.shared.edits.dir控制,在写EditLog时会由两个不同的输出流来控制日志的写过程,分别为:EditLogFileOutputStream(本地输出流)和QuorumOutputStream(JN输出流)。写EditLog也不是直接写到磁盘中,为保证高吞吐,NameNode会分别为EditLogFileOutputStream和QuorumOutputStream定义两个同等大小的Buffer,大小大概是512KB,一个写Buffer(buffCurrent),一个同步Buffer(buffReady),这样可以一边写一边同步,所以EditLog是一个异步写过程,同时也是一个批量同步的过程,避免每写一笔就同步一次日志。
这个是怎么实现边写边同步的呢,这中间其实是有一个缓冲区交换的过程,即bufferCurrent和buffReady在达到条件时会触发交换,如bufferCurrent在达到阈值同时bufferReady的数据又同步完时,bufferReady数据会清空,同时会将bufferCurrent指针指向bufferReady以满足继续写,另外会将bufferReady指针指向bufferCurrent以提供继续同步EditLog。上面过程用流程图就是表示如下:
既然EditLog是异步写的,怎么保证缓存中的数据不丢呢,其实这里虽然是异步,但实际所有日志都需要通过logSync同步成功后才会给client返回成功码,假设某一时刻NameNode不可用了,其内存中的数据其实是未同步成功的,所以client会认为这部分数据未写成功。还有EditLog怎么在多个JN上保持一致的呢?
解决方案:隔离双写
在 ANN 每次同步 EditLog 到 JN 时,先要保证不会有两个NN同时向JN同步日志,也就是说同一时间QJM仅允许一个NameNode向编辑日志中写入数据。这个隔离是怎么做的。这里面涉及一个很重要的概念Epoch Numbers,很多分布式系统都会用到。Epoch有如下几个特性:
但QJM是怎么保证上面的特性的呢,主要有以下几点:
这样就能保证主备NN发生切换时,就算同时向JN同步日志,也能保证日志不会写乱,因为发生切换后,原ANN的EpochNumber肯定是小于新ANN的EpochNumber,所以原ANN向JN的发起的所有同步请求都会拒绝,实现隔离功能,防止了脑裂。
如果在写过程中写失败了,可能各个JN上的EditLog的长度都不一样,需要在开始写之前将不一致的部分恢复。恢复机制如下:
数据恢复后,ANN上会将本地处于in-process状态的日志更名为finalized状态的日志,形式如edits[start-txid][stop-txid]。
日志从ANN同步到JN的过程,具体如下:
通过上面一些步骤,日志能保证成功同步到JN,同时保证JN日志的一致性,进而备NN上同步日志时也能保证数据是完整和一致的。
这个读过程是面向备NN(SNN)的,SNN定期检查JournalNode上EditLog的变化,然后将EditLog拉回本地。SNN上有一个线程StandbyCheckpointer,会定期将SNN上FSImage和EditLog合并,并将合并完的FSImage文件传回主NN(ANN)上,就是所说的Checkpointing过程。下面我们来看下Checkpointing是怎么进行的。
在2.x版本中,已经将原来的由SecondaryNameNode主导的Checkpointing替换成由SNN主导的Checkpointing。下面是一个CheckPoint的流向图:
总的来说,就是在SNN上先检查前置条件,前置条件包括两个方面:距离上次Checkpointing的时间间隔和EditLog中事务条数限制。前置条件任何一个满足都会触发Checkpointing,然后SNN会将最新的NameSpace数据即SNN内存中当前状态的元数据保存到一个临时的fsimage文件( fsimage.ckpt)然后比对从JN上拉到的最新EditLog的事务ID,将fsimage.ckpt_中没有,EditLog中有的所有元数据修改记录合并一起并重命名成新的fsimage文件,同时生成一个md5文件。将最新的fsimage再通过HTTP请求传回ANN。通过定期合并fsimage有什么好处呢,主要有以下几个方面:
在活动namenode(ANN)失效之后,备用namenode(SNN)能够快速(几十秒的时间)实现任务接管,因为最新的状态存储在内存中:包括最新的编辑日志条目和最新的数据块映射信息。实际观察到的失效时间略长一点(需要1分钟左右),这是因为系统需要保守确定活动namenode是否真的失效了。活动namenode失效且备用namenode也失效的情况下,当然这类情况发生的概率非常低非常低的,现在Hadoop 3.X发行版本已经支持运行更多备用namenode来提供更高的容错性。
系统中有一个称为故障转移控制器(failover controller)的新实体,管理着将活动namenode转移为备用namenode的转换过程。有多种故障转移控制器,但默认一种是使用了Zookeeper来确保有且仅有一个活动namenode。每一个namenode运行着一个轻量级的故障转移控制器,其工作就是监视宿主namenode是否失效(通过一个简单的心跳机制实现)并在namenode失效时进行故障转移,这就是HA的主备切换机制,主备选举依赖于Zookeeper。下面是主备切换的状态图:
从图中可以看出,整个切换过程是由ZKFC(即故障转移控制器,全称Zookeeper Failover Controller)来控制的,具体又可分为HealthMonitor、ZKFailoverController和ActiveStandbyElector三个组件。
在故障切换期间,Zookeeper主要是发挥什么作用呢,有以下几点:
在哪些场景会触发自动切换呢,从HDFS-2185中归纳了以下几个场景:
管理员也可以通过手动发起故障转移,例如在进行日常维护时,这称为”平稳的故障转移“(graceful failover),因为故障转移控制器可以组织两个namenode有序地切换角色。命令参考如下所示。
将 active 状态由 nn1 切换到 nn2
- # hdfs haadmin -failover --forcefence --forceactive nn1 nn2
在启用自动故障转移的集群上 --forcefence -- forceactive 参数不起作用
使用以下方法检查名称节点状态(假设 nn1 为 active,nn2 standby):
- # hdfs haadmin -getServiceState nn1 active # hdfs haadmin -getServiceState nn2 standby
于是我们人为制造故障,在 nn1 上查看 NameNode 进程
- # jps
- # kill -9 [进程ID]
自动故障转移将会激活 nn2 节点,状态从 standby 转换为 active
但在非平稳故障转移的情况下,无法确切直到失效NameNode是否已经停止运行。例如网速较慢或者网络被分割的情况下,可能激发故障转移,但Active NameNode依然运行着并且依旧是Active NameNode。高可用实现做了更一步的优化,以确保先前Active NameNode不会执行危害系统并导致系统崩溃的操作,该方法称为”规避“。
规避机制包括:撤销 NameNode 访问共享存储目录的权限(通常使用供应商指定的NFS命令)、通过远程管理命令屏蔽相应的网络端口。最不行的话,可以通过一枪爆头(断电关机)等制造人为故障技术。