云计算(八)-hadoop HA----Quorum Journal 设计

来源:互联网 发布:如何优化关键词 编辑:程序博客网 时间:2024/04/30 06:25

本文是hadoop HA 方案Quorum Journal设计的翻译。原文参考这个链接中的附件:https://issues.apache.org/jira/browse/HDFS-3077


1 概述
1.1 背景
  HDFS-1623和相关的JIRAs加入了对HDFS NameNode高可用性的支持,但是依赖一个共享存储目录,在里面存储共享的edit log。这个共享存储必须是高可用的,并且可被集群中的NameNode同时的访问。
  目前,实现这种共享的编辑目录,推荐的实现是同个NFS挂载NAS设备。这个共享的挂载点允许活跃的NameNode写edit,同时备份的Namenode从尾部读这个edit文件。一旦故障切换发生,它要求有一个自己写的互斥脚本完成这两个功能之一:(a)关掉前一个活跃节点,或者(b)阻止前一个活跃节点访问共享挂载点。

1.2 当前实现的一些局限
  以上的条件大部分环境中时可以满足的,主要的高可用性依赖于这个共享网络设备。这个有两种方式实现,要么用可远程控制的PDU,要么通过NAS设备实现自定义协议。
  然后在有些环境中,这个方案是不好满足的,比如:
  1. 自定义硬盘 - NAS设备和远程控制的PDU非常昂贵,也有别于标准部署。
  2. 复杂的部署 - HDFS安装完成后,管理员必须用额外的步骤去配置NFS挂载,自己写互斥脚本,等。这样复杂的HA部署可能因为一点错误配置导致不可用。
  3. 没有很好的NFS客户端 - 很多linux版本中,NFS客户端的实现都有bug,容易出现误配。例如,管理员很容易弄错挂载选项,导致Namenode被冻结并不可恢复。

1.3 一个替换方案的需求
1.3.1 不同点
  这个设计文档描述了一个替换方案,它满足一下几点需求:
  1. 没有特殊硬件的需求 - 用普通的商品机器就能满足设计,和现有的hadoop集群机器一样就行。
  2. 不需要额外自己弄一些互斥配置 - 所有需要的互斥都由软件完成,构建进系统了。
  3. 没有单点故障 - 作为HA的解决方案,edit logs的存储应该是完全HA的。
  需求3 暗示着edit logs必须存储在多个节点上。往后我们将称呼这些节点为journal replicas。

1.3.2 正确性需求
  当然我们要保证HDFS中任何修改edit logs的正确性需求:
  1. 任何同步了的edit操作必须不能被忘记 - 如果NameNode成功的调用FSEditLog.logSync(),所有的同步的修改必须被持久化的记录下来,即使有任何失败。
  2. 任何还没同步的edit操作可能被忘记,也可能不会 - 如果一个NameNode写一个edit,并且在调用logSnyc()之前或者调用中挂掉,系统可能记住这个edit,也可能忘记。
  3. 如果一个edit被读了,它不能被忘记 - 如果StandbyNode读取尾部的edits并且看到了某条edit,那这条edit就不能被忘记。
  4. 对于任何给定的txid,这必须要恰好有一条可用的事件 - 任何节点用一个给定的ID读事件,然后其它节点用相同的ID读到的数据必须是一样的。
1.3.3 额外目标
  此外,下面的这些不是严格的需求,但是这个设计中保证了:
  1. 可配置任意允许失败的节点数 - 如果管理员想容忍多余一个节点的失败,他可以配置多一点节点来达到他期望的容忍节点数。具体说,配置2N+1个节点,就能够容忍N个节点失败。
  2. 一个慢journal节点不会影响延时 - 如果一个存储edits的节点变慢或者挂掉,系统应该继续操作而没有延时的惩罚。当一个节点挂掉时,我们不应该由于超时而停止客户端的编辑操作。
  3. 添加新的journal节点不会影响延时 - 为了容忍多余一个的失败,管理员会配置5个或者更多的journal节点。和journal节点间的通信应该是并行的,这样添加节点才不会引起延时的线性增长。

1.3.4 运维的需求
  接下来的需求不是和算法相关的,但是对于部署HDFS集群有着重要的意义:
  1. Metrics/日志 - 任何后台进程的引入,应该集成HDFS现有的Metrics和日志系统。这对现存的监控架构是必要的。
  2. 配置 - 任何必要的配置应该和现有的xml一致。
  3. 安全 - 任何跨多节点的操作应该(a)相互授权 和(b)加密,采用现有的Hadoop机制。例如任何IPC/RPC交互应该用SASL-based传输附带Kerberos提供的相互授权。任何ZooKeeper的使用,应该支持ZooKeeper ACLs和授权。

1.4 Quorum-based 方法
  这篇文档描述Quorum Journal Manager的设计,这是上述问题的一个解决方案,它满足上述的所有需求和额外目标。
  这个设计依赖这样一个概念:多个后台进程的quorum commits,我们把这样的后台进程表示为JournalNodes。每一个JournalNode暴露一个简单的RPC接口,允许NameNode读写edit log,这个log是存在本地磁盘上。当NameNode要写一个edit时,它发送这个edit给集群中所有的JournalNodes,等待大多数节点的回复。一旦大多数回复成功,这个edit就被认为提交了。
  接下来详细介绍这个设计。

2 设计-写logs
2.1 总共有两个组件:
  1. QuorumJournalManager, 运行在每个Namenode上(现在的ha集群最多两个Namenode),它通过rpc联系JournalNodes,发送编辑、互斥、同步等命令。
  2. JournalNode进程,运行在N个机器上,暴露hadoop ipc接口允许QuormJournalManager远程写edits到它的本地磁盘上
2.2 QuorumJournalManager工作流
  当QJM想要开始写edit logs时,它会进行以下这些操作:
  1. 互斥写者 - 它必须保证没有其它的QJM在写edit logs。这是一种互斥机制,即使两个Namenodes都认为它自己是活跃状态,并进行写入edit logs,互斥机制会保证只有一个Namenode会写入成功。后面会详细说明这个互斥机制。
  2. 恢复正在写入的logs - 一个写者前一次写logs失败了,它可能造成不同备份中出现不同长度的log(例如:前一个写者只发送了edit给三个JNs中的一个,然后挂掉了)。我们必须先同步logs。
  3. 开始一个新的log段 - 现有的实现中,这是写edit logs的正常流程。
  4. 写edits - 对于每一批edits,写者发送这一批edits给所有的JNs。一旦它接收到超过半数JNs的返回成功,它就认为这次写入成功了。写者维持一个写入过程的pipeline,这样就算临时有一个节点变慢也不会影响整个系统的吞吐和延时。 如果一个JN失败,或者回复得太慢导致超时,这个JN就会被标记为outOfSync,在当前的log段就不再使用这个JN了。只有大于半数的JNs还活着,就不会出问题。之前失败的那个节点,会在下一轮的edit log中被重试。
  5. 完成log段 - 现有的实现中,QJM发送一个完成log段RPC给所有的JNs,当接收到大于半数的JNs确认后,这个log段被认为完成,下一个log段可以开始。
  6. Go to step 3
  后面的章节会对每一步进行详细的解释。

2.3 互斥写者
  为了满足互斥需求,并且不采用定制化硬件,我们需要有能力保证前一个活跃的写者在某一点后不再提交edits。在这个设计中我们引入一个epoch numbers的概念,类似的概念在很多分布式文献中能找到(例如Paxos, ZAB等)。在我们的系统中epoch numbers有如下一些性质:
  • 当一个写者变为活跃时,会分配给它一个epoch number
  • 每一个epoch number都是唯一的,没有任意两个写者有相同的epoch number
  • epoch numbers定义了写者顺序,对于任意的两个写者,epoch numbers定义了一种关系,一个写者被认为更后于另一个写者,当 且仅当它的epoch number更加大一些。
  我们在下面一些情况使用一个epoch number:
  • 在对edit logs做任何改动的时候,QJM必须要被分配一个epoch number
  • QJM发送它的epoch number给所有JNs,包含在消息newEpoch(N)中。它不会用epoch number进行处理,除非大于半数的JournalNodes返回一个成功指示。
  • 当JN回复了这样的请求,它会记录这个epoch number在变量lastPromisedEpoch中,这个变量会被写入磁盘。
  • 任何请求改变edit logs的RPC,必须包含请求者的epoch number。
  • 在任何RPC动作(除了newEpoch())之前,JournalNode拿请求者的epoche number和它自己的lastPromisedEpoch做比较。如果请求者的epoch更小,它就会拒绝这次请求。如果请求者的epoch更大,它会更新自己的lastPromisedEpoch。这会使JN更新自己的lastPromisedEpoch,即使它挂掉的那会有新的的写者变成活跃者。
  这个策略保证,一旦一个QJM接收到一个成功的newEpoch(N)RPC回复,就不会有QJM带着比N小的epoch能更改edit logs在大于一半的节点上。

2.4 写者epochs
  除了保存一个lastPromisedEpoch,每个JournalNode也保存着一个持久化了的epoch LastWriterEpoch,JournalNode更新这个变量在任意新log段开始前,使得可以随时知道上一个写者的epoch。
  这样做得重要性,在接下来讨论段恢复的边界条件时,变得清晰。

2.5 产生epoch numbers
  在上面的章节,我们没有解释QJM怎么产生一个满足所需属性的epoch number。我们这里借用ZAB和Paxos的方法来解决这个问题。我们用下面的算法:
  1. QJM发送getJournalState()给所有JNs。每个JN回复它自己的lastPromisedEpoch。
  2. 接收到大于一半的JNs回复,QJM拿出它接收到的最大值,然后把它加一,这就产生了一个proposedEpoch。
  3. QJM发送newEpoch(proposedEpoch)给所有的JNs,每个JN原子的比较这个建议值和它当前的lastPromisedEpoch。如果这个新的建议值比它存储的值大,则更新它的lastPromisedEpoch为新值,并且返回成功。如果小,则返回失败。
  4. 如果QJM接收到超过一半的JNs的返回成功,则设置它的epoch number为proposedEpoch。否则,它中止尝试成为一个活跃的写者,并抛出一个IOException。这个异常被Namenode同写NFS失败一样的方式处理。-- 如果QJM被用作共享的edits单元,它将会导致Namenode挂掉。
  这里,我们不进行形式化的证明。然而一个粗糙的解释如下:对于相同的epoch number,没有两个写者能够成功的完成第4步,因为所有可能的超过一半的节点会有至少一个的重叠节点。因为没有节点会对同一个epoch number返回成功两次,这个重叠的节点会阻止两个写者中的一个成功完成。
  接下来看一个带注释的日志,记录一个无竞争的例子:

40,319 INFO QJM - Starting recovery process for unclosed journal segments...

  首先NN发送getJournalState()给3个节点。他们各自返回epoch 0 因为这个日志来源于新格式化的系统。
40,320 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:39595:
          getJournalState {jid { identifier: "test-journal" }}
40,323 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:39595:
          getJournalState {lastPromisedEpoch: 0 httpPort: 45029}
40,323 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:36212:
          getJournalState {jid { identifier: "test-journal" }}
40,325 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:36212:
          getJournalState {lastPromisedEpoch: 0 httpPort: 49574}
40,325 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:33664:
          getJournalState {jid { identifier: "test-journal" }}
40,327 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:33664:
          getJournalState {lastPromisedEpoch: 0 httpPort: 36092}

  NN发送newEpoch开始epoch 1,比所有的回复epoch都大。
40,329 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:39595:
          newEpoch {jid { identifier: "test-journal" } nsInfo { .. .} epoch: 1}
40,334 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:39595: newEpoch {}
40,335 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:36212:
          newEpoch {jid { identifier: "test-journal" } nsInfo { .. .} epoch: 1}
40,339 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:36212: newEpoch {}
40,339 TRACE Outgoing IPC) - 1: Call -> null@/127.0.0.1:33664:
          newEpoch {jid { identifier: "test-journal" } nsInfo { .. .} epoch: 1}
40,342 TRACE IPC Response) - 1: Response <- null@/127.0.0.1:33664: newEpoch {}
40,344 INFO QJM - Successfully started new epoch 1

2.6 同步logs
  为了使log同步,写者完成下面几个步骤:
  1. 确定最新log段的事件ID。
  2. 确定最新一个事件ID N已经成功提交在大于一半的节点上。这就是等于确定了哪些JournalNode包含最新写入事件的log。
  3. 确保大于一半的节点同步这个log段,从包括最新段的节点上拷贝。
  4. 命令这些节点标记已完成log段。
  log同步的后置结果有:
  1. 任何之前提交的事件必须存储在大于一半的节点中。
  2. 大于一半的节点同意这些内容,并且以txid作为上一个log段的结束,并且标记为已完成段。
  同步完成事件N后,写者可能开始写新的段,用事件N+1开始。
  log同步算法在文档后面描述。

2.7 常量
  常量 1 一旦log段被标记完成,就不再会被标记非完成。
  常量 2 在任意节点上如果有一个log段开始于txid N,则有大于一半的节点包含一个已完成的log段结束于rxid N-1
  在无失败的情况下,这是对的,因为写者总是完成它的当前log段,再开始一个新的段。标记已完成不会成功,除非超过半数的节点承认已完成。
  在有失败的情况下,这是对的,因为这是log同步的后置结果。在任意写者开始写之前,它必须先进行log同步。
  常量 3 在任意节点上如果有一个已完成的log段结束于txid N,则有大于半数的节点有一个结束于txid N的log段。
  这是对的,因为写者等待大于半数的节点确定最后的修改,才进行已完成操作。

2.8 恢复算法
  当一个新的写者接管的时候,前一个写者很有可能留下一些log段在“正处理”阶段。在写者开始写之前,它必须恢复这些“正处理”段,并把它已完成。这个过程的需求如下:
  • 已完成的log段必须包含所有之前提交的事件。一个事件被认为已提交只有当超过半数的JNs对前一个写者确认过这个事件。
  • 这个log段必须标记为已完成在超过半数的journal节点上。
  • 所有的日志记录者必须完成log段成相同的长度和内容。也就是说,如果有两个日志记录者包含一个已完成的log段开始于相同的事件ID,则这些log文件必须是语义一致的。
  这个问题是一致性的一个方面:对于一个给定段事件ID,我们必须让一组journal节点达到一致,如此以致所有的日志记录者同意这个段的长度和内容,并且完成它。这个设计里采用的方法基于著名的Paxos算法。
  依据Paxos设计回复过程,我们为log运行单个Paxos实例,试着确定哪段内容正在恢复。因为用RPC框架传输整个log段是不实际的,所以我们仅仅传输最大的事件ID和这段内容的md5值,同时传一个URL值,通过这个URL可以下载log段。
  和经典的Paxos有一小点不同的是,我们重用唯一的epoch number,从上面描述的NewEpoch阶段获取的,而不是为恢复处理产生一个新的epoch number。这一点思路和著名的Mutil-Poxas一致。
  1. 确定哪个段需要恢复:依据newEpoch()的返回值,每一个JournalNode发送它的最大的log段事件ID。如果任何log段已经成功的开始在大于一半的节点上,NN就会找出这个段需要恢复(因为返回newEpoch()的这一大半节点和在新段中提交事件的一大半节点是肯定会有重叠的)
  2. PrepareRecovery RPC:写者发送一个RPC给每一个JN要求准备恢复给定的段。没一个JN返回它自己本地磁盘中当前段的状态,包括长度和完成状态(已完成或者正处理)。
    这个RPC请求和回复分别对应Paxos中的Prepare(Phase 1a)和Promise(Phase 1b)。
  3. AcceptRecovery RPC:基于PrepareRecovery的回复,写者指定一个段(这里是指一个JN)作为恢复的源。选出这个段是根据它必须包含所有之前提交的事件。这个决定的细节会在下面的2.9节描述。
    选择源后,写者发送AcceptRecovery RPC给每一个JournalNode,包含两个东西,段状态和可以获得这个log段拷贝的URL。
    AcceptRecovery RPC对应Paxos的Phase 2a,常叫做Accept。
    接收到AcceptRecovery RPC,JournalNode将会做以下动作:
    (a)Log同步:如果当前的磁盘log丢失,或者是长度不同,JN会从URL中下载log,替换掉当前的log。
    (b)持久化恢复元数据:JN写入磁盘一个数据结构,这个数据结构包含段状态和写者的epoch number。在未    
         来,对这个段的PrepareRecovery调用,这个状态和epoch number将被返回。
    如果这些动作成功的完成,JournalNode返回成功给写者。如果写者接收到大多数JournalNode的返回成功,它就会往后继续。
    JournalNode回复AcceptRecovery的行为,对应Paxos的Phase 2b。
  4. 完成段:这个阶段,写者知道超过半数的JournalNodes已经有了一致的log段,并且持久化了恢复信息。因此,将来任何写者发起PrepareRecovery将会看到这次的决定并作出相同的结果。我们现在可以安全的完成这个log段(类似于Paxos的Commit phase)。写者简单的发起一个FinalizeLogSegment调用给每个JournalNodes。
    当接到这个时,JournalNodes设置log段标志为已完成。他们都可以删除掉已经持久化的段信息,因为已完成状态本身信息量已经足够了,对于做恢复决定。
2.9 Journal同步 - 选择恢复源
  在上一节中,我们隐藏了客户端如何选择哪个log段(对应的JournalNode)作为恢复的源。现在来表述整个协议的过程:
  一接到PrepareRecovery()的回复,NN按下面的规则评估他们:
  1. 如果一个节点回复它没有段开始于给定的事件ID,它不能成为恢复源。
  2. 如果有节点已经完成了段,这表示前一次的恢复已经提交,没有再进行恢复的必要。这种情况下,包含已完成段的这个节点作为源。
  3. 对于任何两个返回正在进行段的节点,他们按下面方式比较:
    (a)对每个JN,认为maxSeenEpoch比他的lastWriterEpoch更大,并且epoch number与之前任何接受了的恢复建议一样。
    (b)如果一个JN的maxSeenEpoch比另一个的更大,它就会是一个更好的恢复源。这个解释将在下节2.10.6中。
    (c)如果两个值相等,哪个JN的事件更多,就会被认为是更好的恢复源。
  注意,这里可能会有多个段(就是说多个JournalNodes)被认为是相同好的源。例如,如果所有的JournalNodes提交的最近的事件,并且没有更新的事件被部分的建议,所有的JournalNodes被认为是一致的状态。
  在这样的情况下,现在的实现是随机的选择一个节点为源。当JournalNode接收到acceptRecovery() RPC时,看到已经有一个一致的段存在它的磁盘上,它不会去下载log的。当所有的节点段一致时,就没有log数据传输。

2.10 同步logs - 示例
2.10.1 正处理log不一致 - 大部分节点成功
  这种情况是,前一个写者发送了一批三个事件给JN2和JN3,在发送给JN1之前它挂掉了。JN1有了一点延后。
JN segment last txidlastWriterEpoch
JN1 
JN2
JN3 
edits_inprogress_101
edits_inprogress_101
edits_inprogress_101
150
153
1531
1
1

  因为写者成功的写入事件153到大于半数的节点,我们必须通过153恢复,以满足正确性需求。因为所有的节点有相同的maxSeenEpoch,我们遵照规则3c来确定备份源为JN2或者JN3。

2.10.2 正处理log不一致 - 还没有大部分节点成功
  这种情况是,前一个写者发送了一批三个事件给JN2,然后在发送给JN1或JN3之前挂了。这个例子中,JN3慢了很长一段时间,落后得有点多。
JN segment last txidlastWriterEpoch
JN1 
JN2
JN3 
edits_inprogress_101
edits_inprogress_101
edits_inprogress_101
150
153
1251
1
1

  因为前一个写者没有写成功多数节点,事件150和事件153都可能是一个正确的选择。125不会是个正确的选择,因为150事件包含在大多数节点中。
  如果恢复中我们只看见了JN1和JN2,我们会恢复到事件153。如果我们只看到了JN1和JN3我们会恢复到150。如果我们看到JN2和JN3我们会恢复到153。这个决定遵照规则3c。

2.10.3 已完成log不一致 - 大部分节点成功
  这种情况是,前一个写者发送了finalizeEditLog调用给JN1和JN2,在发送给JN3之前挂掉了。JN3有一点延后。
JNsegmentlast txidJN1
JN2
JN3edits_101-150
edits_101-150
edits_inprogress_101150
150
145

  这种情况,我需要命令JN3从JN1或者JN2上同步。任何的一个超过半数的集合都会看到一个已完成的段,因此我们会决定源是JN1或JN2,遵照规则2

2.10.4 已完成log不一致 - 大部分节点未成功
  这种情况是,前一个写者发送了finalizeEditLog(101,150)给JN1,在发送给JN2或JN3之前挂掉了。
JNsegmentlast txidJN1
JN2
JN3edits_101-150
edits_inprogress_101
edits_inprogress_101150
150
125

  如果我们有从JN1接收到回复,遵照规则2,它将会被选中。如果只收到JN2和JN3的回复,遵照规则3,JN2会被选中。
  它保证JN2或JN3有已完成log的全部长度,因为QJM在调用已完成之前,已经得到大部分节点的最后edit。

2.10.5 开始log不一致 - 大部分节点没开始
  这种情况是,QJM没有获得大部分节点的startLogSegment(151),因为它发送RPC给JN1后就挂了。
JNprev segmentcur segmentlast txidJN1
JN2
JN3edits_101-150
edits_101-150
edits_101-150edits_inprogress_151
-
-150
150
150

  这个case,我们在JournalNode的代码中用了一个简单的trick:在恢复中,如果段本身完全是空的,我们假装它不存在(因为它没包含有用的数据)。这个case中,所有三个JournalNodes就是一样的了,不需要恢复。

2.10.6 第一批log不一致 - 大部分节点没log
  这个情况是,QJM先写了一个事件ID 150,然后成功的已完成log段在所有节点上。接着它发起startLogSegment(151),在所有节点上成功。接着写一组事件(151-153),但是只在JN1上成功。
JNprev segmentcur segmentlast txidlast WriterEpochJN1
JN2
JN3edits_101-150
edits_101-150
edits_101-150edits_inprogress_151
-
-153
150
1501
1
1

  注意,尽管edits_inprogress_151在JN2和JN3已经被创建,由于是空文件,这里就把它移除了。
  想象恢复过程只有JN2和JN3存在,新的NameNode(with writer epoch 2)成功的提交了一个事件。我们得到下面这个状态:
JNprev segmentcur segmentlast txidlast WriterEpochJN1
JN2
JN3edits_101-150
edits_101-150
edits_101-150edits_inprogress_151
edits_inprogress_151
edits_inprogress_151153
151
1511
2
2

  注意,JN2和JN3的lastWriterEpoch被设置成2,因为新的写者已经开始写了。
  如果这是我们挂掉,再进行恢复,恢复到txid 151而不是txid 153,尽管JN1的log比JN2和JN3的log更长。这就是lastWriterEpoch的目的:因为JN2和JN3有更高的Writer epoch,他们胜于JN1根据规则3b。因此,JN1的log被移除,用JN2或JN3上的数据替换。

2.10.7 多次恢复 - 第一次失败
  这个情况就是记录已接受的恢复元数据的目的。
  假设我们失败在三个节点都有不同长度,例如2.10.2
JN segment last txidacceptedInEpochlastWriterEpoch
JN1 
JN2
JN3 
edits_inprogress_101
edits_inprogress_101
edits_inprogress_101
150
153
125-
-
-1
1
1

  现在假设第一次恢复只联系了JN1和JN3。它决定的长度是150,调用acceptRecovery(150)给JN1和JN3,接着finalizeLogSegment(101-150)。但是他挂了在finalizeLogSegment到达JN1之前。现在的状态:
JN segment last txidacceptedInEpochlastWriterEpoch
JN1 
JN2
JN3 
edits_inprogress_101
edits_inprogress_101
edits_101-150
150
153
1502
-
-1
1
1

  接着,新的NN开始恢复,假设他只更JN1和JN2通信。如果他不考虑acceptedInEpoch,他会作出一个错误的决定,并最后finalize txid 153,这破坏了常量,即已完成的log段开始于相同的事件ID必须是相同的长度。由于规则3b,他会选择JN1作为恢复源,并正确的finalize JN1和JN2在txid 150而不是txid 153,这就和JN3一致了。

2.11 写edits
  写edits相对来说过程比较简单。QJM用下面几步:
  1. 一旦logSync,拷贝队列中的数据到一个新的数组中。
  2. 推送这些数据到每个远程的JournalNode的队列中。
  3. 每个JN有个线程处理。这些线程发起logEdits RPCs给其它的JournalNodes。
  4. 一旦接到,JournalNode (a)验证epoch number (b)验证每一批edits的事件ID,保证不会出现乱序和掉包 (c)写入并同步edits到当前的log段 (d)返回成功。
  5. 最初的logSync线程等待一个超过半数的成功回复。如果超过半数的回复带有异常,或者超时,logSync()调用会抛出exception。这种情况下,QJM被用作共享的存储机制,它会引起NN的abort。

3 设计-读logs
  在第一版的实现中,我们做一个严格的限制,一个log段只有在完成log段这个操作后才能被读到。因为log段只有被多于半数的节点同意完成后,读者从任一个备份上读取的已完成log段,才确保它和其它副本是一致的。
  为了实现这个,每个JN暴露一个http server,允许远程进程流式读取任一个已完成的log段,同时暴露一个RPC,允许远程进程枚举可用的log段。
  当后备节点(StandbyNode)从JN上读的时候,它的第一件事是getEditLogManifest()RPC所有的节点。所有已完成的段返回并且合并在一起RedundantEditLogInputStreams,这样备份节点可以从任一节点上读取每一段。如果有一个JN在读取中失败,冗余的输入流可以自动的选择一个拥有相同段的不同节点恢复。
出自:http://blog.csdn.net/albani/article/details/8194821