Hadoop HA解析

架构师 来源:Pun_C 29℃ 0评论

HDFS采用的是fsimage + edits的存储方式,fsimage是某个时间的内存文件系统镜像,edits是修改操作,每个修改操作称为一个事务,有一个整形的事务id指定。checkpoint的时候就存储一次fsimage,同时可以删除之前的edits。另外edits切割为很多segement,不同的segment都包含一段修改操作记录,正在写入的segment的文件名有inprogress和起始结束的txid修饰,写入完成就只有起始结束的txid修饰。recover的时候先把最新的fsimage加载到内存,然后回放对应的edits文件,这样就恢复到最新状态。整个流程类似于数据库的redo log。

HDFS为了提高可用性,提供了SecondaryNameNode,NFS,QuorumJournalManager等几种方法。

SecondaryNameNode

    SecondaryNameNode定期从NameNode拉取fsimage和edits文件,NameNode停止写入edit,写入新的edit.new。SecondaryNameNode然后加载fsimage,合并edits之后生成新的fsimage2,把fsimage2复制到NameNode中,NameNode替换原来的edits和fsimage。然后把edit.new改名回edit。
可以手动从SecondaryNameNode恢复之前checkpoint的数据。
作用就是帮NameNode进行check point。没有failover。

Using the NFS

热备。Active NameNode和StandBy NameNode之间有共享的存储NFS。Active会把修改写入NFS的edit,StandBy会看到这个修改,然后应用到自身的namespace,通过这样保持与Active的同步。
DataNode需要向两个NameNode发送Block信息。
为了避免split brain,需要设置fencing方法,当failover的时候,禁止之前的Active继续向NFS写入edit。

 

Using the Quorum Journal Manager

基于Paxos的Quorum-based方法。
整个流程和Using the NFS类似,使用JournalNode作为共享存储。
包含以下模块:
1. NameNode中运行的QuorumJournalManager。负责通过RPC和JournalNode通信,发送edits,执行fencing和同步等操作。分为Active NN和StandBy NN两种状态的NN。
2. JournalNode守护进程运行在N(N>=3)台机器上。每个进程都允许QuourumJournalManager写入修改到本地中。
其中fsimage保存NameNode的本地,JN只负责存储edits内容。
另外算法必须保证
1.任何同步的修改不能lost;
2.任何没有同步的修改可能lost也可能生效;
3.如果edit被读取,那么就一定不能lost;
4.对于指定的txid,必须只有一个有效的事务。
Active NN正常运行时,流程如下:

往JournalNode写入Edits(QJournalProtocol.journal)

NameNode进行logSync时,QJM把队列的batch edits的byte数组复制到新的数组后发送到所有的JoualNode中。
JN收到edits后,需要进行一些校验:
(a)校验epoch number是否大于等于lastPromisedEpoch (b)校验epoch number是否等于lastWriterEpoch (c)校验请求的segmentTxID是否等于当前log segment的txID  (d)校验batch edits的firstTxnId是否等于上一次写入Edits的transactionsID + 1(nextTxId) 
其中a是fencing机制,避免之前active的NN进行修改,如果大于lastPromisedEpoch,则更新并保存到本地;b是保证lastWriterEpoch的正确,用于recover;c是由于每个log segment都有txid,保证每个segment的内容一致;d是保证segment的edits是按照txid有序,也是保证内容一致;
校验之后,把edits写入当前log segment,然后返回success。如果JN的校验失败,会抛出OutOfSync或者IO异常,NN会标记这个JN停止后续对其发送RPC修改,直到startLogSegment才重新写入该JN。
NameNode在执行logSync的线程中,等待quorum节点数返回。如果quorum节点数返回异常或者超时,logSync()跑出异常。这样QJM就会导致NameNode退出。
注意:并不保证正在写的log segment状态一致。

Finalize log segment (QJournalProtocol.finalizeLogSegment)

NameNode发现log segment的txid数量超过阈值等需要roll edit log的时候,就需要进行Finalize log segment操作。同时,由于确保所有finalize的log segment的内容必须一致,并且不能被修改,因此也是log synchronize的操作。 
首先令第一个写log segment的txid作为该log segment的txid,发送该txid以及最后一次写的txid作为参数,RPC请求所有的JN进行finalizeLogSegment。
JN收到请求后,同样进行(a),(b)的校验,然后如果当前log segment的txid等于请求的txid,并且最后一次写的txid也相等,则重命名当前的in progress的log segment,表示完成finalize。并且同时删除之前paxos记录的对应文件?
NameNode等待quorum节点数返回。
注意:保证finalize后的log segment是不变的且存在quorum节点数中其内容保证一致。因此,StandBy NN只会定期读取quorum节点数的finalize log segment,应用到自身的镜像中,其状态会有一定延迟。

Start new log segment (QJournalProtocol.startLogSegment)

roll edit log完成finalize log segment之后就需要start new log segment。注意如果JN是OutOfSync状态,此RPC会发送。
NameNode把上一次写的txid + 1作为log segment txid参数发送RPC到所有的JN中。
JN收到请求后,同样进行(a),(b)的校验,如果当前segment没有被finalize,放弃当前segment。然后更新lastWriterEpoch为当前epoch。最后创建新的log segment。
NameNode等待quorum节点数返回。

Active NN  log recover流程如下:
如果Active NameNode挂掉,则StandBy NameNode收到transitionToActive的RPC请求后,首先要重新创建所有分别连接JN的IPCLoggerChannel,准备写入edits,然后需要fencing old active namenode

Fencing old Active NameNode

HDFS的fencing机制依靠的是epoch number。NameNode变为active的时候,都会创建一个唯一的严格递增epoch number,因此拥有更大的epoch number表示当前的NameNode是最近的Active NameNode。NameNode请求JN修改edits时,都会带上自己的epoch number作为参数,JN会把这个参数与自己保存的lastPromisedEpoch比较,小于则是old active NN的请求,则拒绝;等于则接收修改,大于则接受修改并更新当前值保存到disk。
创建epoch number(createNewUniqueEpoch)算法如下:
* QJM发送getJournalState到所有的JN中。JN返回lastPromisedEpoch。QJM取quorum节点数返回的最大值,然后把这个最大值 + 1作为参数;
* QJM发送newEpoch到所有的JN中,JN把参数与lastPromisedEpoch比较,小于等于则返回异常,大于则更新lastPromisedEpoch,另外扫描所有的edits文件,返回最新segment的segmentTxId。QJM取quorum节点数返回的最大segmentTxId作为后续恢复的segmentTxId。

Recovering in-progress logs

Active NN挂掉后,由于之前正在写的log segment是无法保证一致性,因此要进行recover操作。主要是把quorum节点数上的log segment进行同步,然后finalize保证一致性,这样StandBy NN才可以读取log segment恢复到最新状态,从而成为Active NN对外提供服务。

Recover算法如下:

prepareRecovery 

QJM把newEpoch选取的segmentTxId作为恢复参数发送prepareRecovery到所有JN中,JN进行(a)(b)校验,首先查看是否存在之前acceptRecovery中保存的paxosData,如果存在则尝试把http下载的临时文件替换inprogress的log segment。然后查看是否存在segmentTxId对应的log segment,存在则返回其起始结束的txid以及是否inprogress信息;如果存在segmentTxId的恢复信息paxosData并且对应log segment没有被finalize,则把之前信息保存的segment状态以及epoch number作为acceptedInEpoch返回;同时返回lastWriterEpoch以及committedTxnId。(Phase 1a, 1b in Paxos)。
QJM收到response后,需要选择最佳JN作为恢复源,选择条件如下:
1. 有log segment的比没有的更好,否则继续比较
2. log segment被finalize的比没有的更佳,否则继续比较
3. 此时两者均为in-progress log segment。令seenEpoch为修改过edit的最大epoch (acceptedInEpoch和lastWriteEpoch最大值),则seenEpoch值越大更佳,否则继续比较
4. 比较log segment的结束txId。

取最佳的JN中的一个作为恢复源

acceptRecovery 

QJM把最佳的JN的URL以及log segment state作为参数发送acceptRecovery到所有的JN。JN进行(a)(b)校验,然后如果不存在segmentTxId的log segment,或者该log segment的的长度要小,则通过Http下载最佳JN的log segment。下载到临时文件后,JN会把这次recovery的数据(epoch和segmentState)作为paxosData保存到本地,保存成功后才把临时文件替换原来inprogress的log segment。这样做可以在下载到替换的过程中发生崩溃后,重新recover时prepareRecovery能直接替换inprogress的log segment。(Phase 2a, 2b in Paxos)

finalizeLogSegment

QJM把上面recovery有quorum节点数同步的log segment的startTxId和endTxId作为参数发送finalizeLogSegment到所有的JN。JN会对自身的segment进行校验,如果txId不符合,则会放弃finalize的操作,避免不一致。finalize完成后,JN会把之前保存的paxosData删除,这是由于finalize的log segment和paxosData都能够正确表示recover的选择。
recover过程完成后,StandBy NN调用EditlogTailer.catchupDuringFailover从JN上读取edits,应用到内存的fsimage中。
注意
recover过程可能导致某些没有同步的修改生效

Using the Quorum Journal Manager with automatic failover

虽然使用QJM的方法可以提供可靠的共享存储,但仍然需要manual failover,为了达到automatic failover,需要依赖另外一个自动检测NN故障并且出触发failover的模块。于是,官方提出了基于Zookeeper的automatic failover的方法。
包含三个模块:
1. HealthMonitor。定期检测NN是否正常提供服务
2. ActiveStandbyElector。负责管理和监控ZK。
3. ZKFailoverController。接收HealthMonitor和ActiveStandbyElector的消息并且管理NN状态。另外注意fecing处于中间状态的NN。
目前这三个模块在不同于NN的一个独立的JVM中,大致如下:

HealthMonitor

HealthMonitor主要负责定期向本地的NN发送monitorHealth的RPC。NN收到monitorHealth的RPC后,会检查是否有足够的资源,目前是是否有足够的硬盘空间,如果没有则抛出异常。HealthMonitor根据NN的RPC返回决定其是否HEALTHY,然后通过callback通知ZKFC状态的变化。

ActiveStandbyElector

ActiveStandbyElector主要负责ZK的相关操作。ZKFC主要调用其两个接口:
* joinElection
开始进行Active NN的选举。具体是在ZK的LockFilePath异步创建临时znode。如果ZK通知创建成功,则需要fence old Active NN。(如果使用QJM作为共享存储,这里应该不需要)
fence的做法是首先同步从ZK的BreadCrumbPath获取old Active NN的信息,以其为target作为参数回调ZKFC的fenceOldActive方法。ZKFC首先尝试往target发送transitionToStandby的RPC,如果timeout或者失败,则调用配置的fence method(例如SSH)强制kill掉old Active NN。
如果fence失败,则sleep一段时间后再重新joinElection,目的是让其它NN有机会becomeActive;如果fence成功后,则把local NN的信息同步写入ZK的BreadCrumbPath,这样failover让其它NN对local NN进行fence。之后就回调ZKFC的becomeActive方法,ZKFC调用transitionToActive RPC通知local NN,于是就到NN的Active NN  log recover流程。接着,ActiveStandbyElector监控ZK的LockFilePath状态,以便探知Active NN的状态。
如果ZK通知已经创建LockFilePath,已经有其它NN获得了lock,则回调ZKFC的becomeStandby,ZKFC调用transitionToStandby RPC通知local NN,让其转换为StandBy状态。
* quitElection
退出NN选举。具体是停止ZK的连接。这样之前创建的LockFilePath的临时znode就会被ZK删掉。其它NN就会探知到,便会去创建znode。

ZKFailoverController

ZKFailoverController相对简单,主要是初始化HealthMonitor以及ActiveStandbyElector模块,然后等待HealthMonitor和ActiveStandbyElector的回调,作出具体的逻辑,前面已经有介绍,这里不再赘述。可以看到,ZKFC就是相当于一个controller的角色。


Reference
https://issues.apache.org/jira/secure/attachment/12547598/qjournal-design.pdf
https://issues.apache.org/jira/secure/attachment/12521279/zkfc-design.pdf
https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/