QJM是QuorumJournalManager的简介,是Hadoop V2中的namenode的默认HA方案。qjm方案简单,只有两个组件:journal node和libqjm,qjm方案并不负责选主,选主交由外部实现,例如基于zookeeper实现。libqjm负责journal数据的读写,其中包括journal在异常情况下的一致性恢复;journalnode负责log数据的存储。
QJM的Recovery算法是一个basic paxos实现,用于协商确认最新的in progress log segment的内容(实际操作是确认log segment中的txid范围),因为已经进行了选主,写入的edit log可以按照Multi-Paxos进行优化,跳过basic paxos中的Prepare阶段直接进入Accept阶段,即向2n+1个节点写入n+1个节点返回成功即成功,来降低写入数据内容协议一致的延迟,这样其延迟为:median(Latency @JNs)。
上面的描述很简单,但是下面有很多非常有意思的问题:
QJM分布式协议是基于paxos的实现,能够解决上述问题,并确保数据在节点间的一致。具体来讲,使用Multi-Paxos进行commit每批edit log,使用Paxos进行standy namenode进行failover时的recovery。
http://blog.cloudera.com/blog/2012/10/quorum-based-journaling-in-cdh4-1/
整体QJM的流程:
Fencing是分布式系统中解决“脑裂”问题的解药,新的active namenode能保证老的active namenode不会再修改系统元信息。QJM中Fencing的关键在于epoch number,具有如下属性:
Epoch number的实现流程如下:
上面的这些策略可以保证:一旦QJM收到newEpoch(N)的多数成功应答,那么任何epoch小于N的writer都不会成功写入多数节点。这样就能够解决两个namenode的脑裂问题。
上面的lastPromisedEpoch主要用来进行fencing,并影响AcceptedEpoch(每一轮的promisedEpoch肯定会递增,Prepare的时候取当时的promisedEpoch作为AcceptEpoch)。JournalNode上除了lastPromisedEpoch还需要一个lastWriterEpoch,用于recovery的时候比较unfinialized的edits数据版本。
上面流程中有一个问题没有解释清楚,就是QJM如何设置初始epoch number,这里面就有一个生成epoch的流程:
在讨论recovery之前应该先讨论有些系统依赖的不变量:
这些系统依赖的不变量会影响后面recovery source选取的策略。
当一个新的writer启动之后,上一个writer可能还留下部分log segment处于in progress状态。新writer在写入新的edit log之前,需要对in progress的log segment进行recovery,并进行finialize。因此recovery的工作就是要保证各个JN对in progress的logsegment达成一致,并进行finialize。Recovery算法是一个Paxos过程,利用少数服从多数、后者认同前者的原则,保证in progress的数据一致性。
具体Recovery的算法如下:
每个JN在响应newEpoch的时候都会返回最新log segment的transaction id。
QJM向每个JN发送PrepareRecovery请求获取最后一个log segment的状态,包括长度和是否finialized。如果JN上journal id状态为accepted,那么就返回上次accept的writer的epoch。
这个请求和应答对应到paxos的Prepare (Phase 1a) 和 Promise (Phase 1b)
QJM收到PrepareRecovery的应答之后,新的writer选择一个JN作为source进行同步,其必须包含之前已经committed的transactions。AcceptRecovery RPC中包含segment的状态和source的URL。
AcceptRecovery对应到Paxos中的Phase 2a,通常叫做Accept。
当JN收到AcceptRecovery的时候,进行如下操作:
这个时候多数JournalNodes已经有相同的log segment了,并且都持久化了recovery metadata,即使后面又有PrepareRecovery最终都会得到相同的结果。最后只需要简单的调用FinializeLogSegment即可。
从上面的Recovery算法可以看出,关系到数据安全性最核心的就是接收到PrepareRecovery应答之后,QJM进行Recovery Source的选取。选取规则如下:
a) 对于每个JN,以PrepareRecovery应答中的lastWriterEpoch和AcceptEpoch的最大值作为maxSeenEpoch
b) 如果有一个JN的maxSeenEpoch大于其他JN,那么选择这个JN作为source
c) 如果maxSeenEpoch相同,选择transaction id更大(拥有更多的edit log)的JN作为source
【讨论】为什么这里选取transaction id最大的而不是像AFS那样选取长度最长的n+1个副本中的最小值?因为hdfs中提供了logSync和log两种接口,log是一种异步的接口,可能会出现已经回复了用户,但是没有写到后端的情况,这个时候尽量恢复更多的edit log能降低log丢失的风险。
当一批edits开始写入的时候,QJM会同时向全部JN节点发送请求,JN收到journal请求会会进行如下操作:
QJM会等待多数JN的应答,只有多数JN节点应答成功才返回成功。如果某个JN挂掉或返回失败,将其标记为“out of sync”,当前log segment的后续journal请求不再向其发送。
QJM中只有finialized的log segment才可读,因为QJM已经保证了finialized log segment在多数JN上已经对其内容达成一致。如果要读取in progress的log segment,虽然log segment前半部分数据可能已经达成一致,但是整体内容并没有达成一致,QJM并不知道一致点的位置,需要读取多数JN,根据epoch和txid的大小才能确定,数据是否已经在多数JN上达成一致,跟Recovery算法流程差不多,实现较复杂。
当前QJM在读取的时候,先向全部JN发送一个getEditLogManifest(),获取finialized的segment视图,JN暴露http接口来获取finialized的log segment数据,QJM建立RedundantEditLogInputStreams进行读取。