QJM 核心源代码解读 Hadoop namenode 高可用性分析
背景介绍
HDFS namenode 在接受写操作时会记录日志,最早 HDFS 日志写本地,每次重启或出现故障后重启,通过本地镜像文件+操作日志,就能还原到宕机之前的状态,不会出现数据不一致。如果要做高可用 (HA),日志写在单个机器上,这个机器磁盘出现问题,重启就恢复不了,导致数据不一致,出现的现象就是新建的文件不存在,删除成功的还在等诡异现象。这是分布式存储系统不能容忍的。
在单机系统上是通过 WAL(write ahead log)日志来保证出问题后可恢复,在 HDFS 上对应的就是操作日志(EditLog),用于记录每次操作的行为描述。这里我们简单介绍下 editlog 的格式。
文件格式
-
编辑中的日志 edits_inprogress_txid,也就是后文提到的 segment,txid 代表该日志文件的第一个事务 ID
-
Finalized 日志即一致不再更改的日志文件 edits_fristTxit_endTxid
内容格式
文件头:有版本号 + 一个事务头标识
文件内容
1 操作类型 - 占1个字节
2 日志长度 - 占4个字节
3 事务txid - 占8个字节
4 具体内容
5 checksum - 4个字节
文件结尾:一位事务标识
注意之前没有 journal 分布式日志时,每次 flush 日志时在该段日志后面加一个标识 INVALID_TXID,在下次 flush 时会覆盖该标识,但目前的版本去掉了这个标识
通过 editlog 能做到单机版系统的可靠性,但是在分布式环境下,要保证namenode 的高可用,至少需要两台 namemode。要做到高可用,高可靠,首先就是保证 HDFS 的操作日志 (EditLog) 有副本。但有了副本就引入了新的问题,多个副本之间的一致性怎么保证,这是分布式存储必须解决的问题。 为此 Clouder 公司开发了 QJM(Quorum Journal Manager)来解决这个问题。
Journal Node 集群
Journal node 是根据 paxos 思想来设计的,只有写到一半以上返回成功,就算本次写成功。所以 journal 需要部署 3 台组成一个集群,核心思想是过半Quorum,异步写到多个 Journal Node。
写日志过程
editlog 写入到多个 node 的过程简单描述如下:
-
ActiveNamenode 写日志到 Journal Node,采用 RPC 长连接
-
StandbyNamenode 同步已经 Finally 日志生成镜像文件,以及 Journal Node 直接同步数据,采用 HTTP
ActiveNamenode 每接收到事务请求时,都会先写日志,这个写日志的过程,网上有好多好的文章做分析,这里只是大概说下值得我们学习的地方以及一些好的设计思想。
1 批量刷磁盘
这个应该说是写日志的通用做法,如果每来一条日志都刷磁盘,效率很低,如果批量刷盘,就能合并很多小 IO(类似 MySQL 的 group commit)
2 双缓冲区切换
bufCurrent 日志写入缓冲区
bufReady 即将刷磁盘的缓冲区
如果没有双缓冲区,我们写日志缓冲区满了,就要强制刷磁盘,我们知道刷磁盘不仅是写到操作系统内核缓冲区,还要刷到磁盘设备上,这是相当费时的操作,引入双缓冲区,在刷磁盘操作和写日志操作可以并发执行,大大提高了Namenode的吞吐量。
恢复数据
恢复数据是在 Active Namenode crash 后,standby namenode 接管后,需要变为 Active Namenode 后需要做的第一件事就是恢复前任 active namenode crash 时导致 editlog 在 journal node 的数据不一致。所以在 standby node 可以正式对外宣布可以工作时,需要让 journal node 集群的数据达到一致,下面主要分析恢复算法, 恢复算法官方说是根据 multi paxos 算法 。
Multi Paxos
Paxos 协议是分布式系统里面最为复杂的一个协议,网上主要都是讲概念和理论,不较少讲实践的,所以写本文也是为了更好的理 paxos。paxos 的资料网上很多,可以看登博最近分享的 ppt,讲得很通俗易懂的。
Multi Paxos 是 paxos 改进版,因为 Basic paxos 每一轮 paxos 都生成一个新的 proposal,这一般是由多点写,就像 zk Leader 选举,每个人都可以发起选举。但我们大多数分布式系统都有一个 leader,并且都是有 leader 发起 proposal,那后面就可以用第一次 proposal number,就直接执行 accept 阶段,从 qjm 这个实践里看,有点类似 RAFT 了,都有 leader 的角色。重用当前的提案编号 epoch
恢复数据过程:
1 隔离
2 选择恢复源
3 恢复
1 隔离
开始恢复前需要对前任隔离起来,防止他突然间复活,导致脑裂。隔离的措施是 newEpoch,重新生成一个新的 epoch,算法是通过计算所有 jn 节点中最大的一个,加 1,然后让命令 journal node 集群更新 epoch。更新后,如果前任复活,也不能向 journal node 集群写数据了,因为他的 epoch 比 journal 集群小,都会被拒绝。
生成新的 Epoch 代码如下:
拒绝的代码如下:
2 选择一个恢复源
隔离成功后,需要选择一个副本来恢复,每个 journal 的最新的 segment 文件不一致,因为 namenode crash 的时间不同而不同。所以需要从 journal 集群中最新的副本的信息。
3 恢复
隔离成功后,就开始恢复。在分布式系统,为了使各个节点的数据达成一致,经典的算法还是 Paxos,根据Paxos,分为 2 阶段分别说明如下:QJM 的两阶段对应的是 PrepareRecover 和 AccepteRecover,注意这里说是 Paxos 上文说是 Multi Paxos,区别就是 epoch 重用的。核心算法还是 Paxos。
3.1 PrepareRecovery
向所有 journal node 发送提议,并选中一个恢复的 segment,返回 segment 如下信息:
-
是否有 segment
-
有 segment,则附加 segment 的状态
-
committedTxnId 该 journal node 已经提交的事务 ID,QJM 每次日志同步后,会更新每个 AsyncLogger 的 committedTxnId,journal node 也每次请求都检查传过来的 committedTxnId,如果大于,则更新到本地。
-
lastWriterEpoch 最新的日志文件对应的编号,会每次在写新的 segment,即 startLogSegment RPC 调用时,会记录或者更新
-
AcceptedInEpoch 上次恢复接受的提案编号,在 accept 阶段持久化 ,什么时候 AcceptedInEpoch 会大于 LastWriterEpoch?,当在一次 paxos 协议执行到 accept 都成功,执行恢复前假设 epoch 是 1, lastWriterEpoch 也是 1,则当前的 epoch 是 2( newEpoch)但是在最后 finalize 时,在发给最后一个 journal node 时 ActiveNamenode 又 crash 了,这时这个没有收到 finalize 请求的,他的 AcceptedInEpoch 是 2,他的 lastWriterEpoch 还是 1,因为还没有 stargLogSegment,所以还是 1,这种情况下下次再执行 paxos 恢复时,应该恢复 AcceptedInEpoch 对应的 segemnt,这也是在 2 段提交 (2PC) 在 commit 阶段出现故障时,保障一致性的一种容错方式,值得借鉴。
3.2 AccepteRecovery
根据 PrepareRecovery 选择的结果根据一个算法,选中一个segment,给所有的journal 发送 accept 请求,告诉他们都要和指定的 segment 达到一致,怎么样达成一致,下面会分析到。
PrepareRecover 对应 Paxos 的第一阶段,AccepteRecover 对应第二阶段
在分析具体的2PC实现之前,先上个图,了解下大概流程
上图主要包含的流程总结如下
-
Prepare Recovery
-
PrepareRecoverRequest
-
prepareResponse
-
checkRequest 并选择一个 segment 来做为同步源
-
-
Accept Recovery
-
客户端发起AcceptRecovery
-
Journal 接受 AcceptRecovery 请求
-
接受请求后的检查 segment 是否包含事务
-
接受请求后的检查上一次 paxos 是否正常完成,这里的检查是判断是否需要去同步数据
-
-
commit
-
这里分别对每个阶段的主要行为分析如下:
PrepareRecoverRequest(P1a)
第一阶段,发起提案
服务端 Journal(prepareResponse) P1b:
checkRequest
journal 在newEpoch,发起提案,接受提案都通过 checkRequest 来检查提案编号epoch,的合法性,并做对应的操作
选择一个 segment 来做为同步源
第一阶段准备恢复完成后,如果超过半数以上的节点返回,则需要从这些返回的日志文件segment中选择一个最合适的副本。下面就是选择算法
选择的算法如下:
-
近可能选择一个存在segment的文件来恢复,因为有的 journal node 可能不包含对应的 segment
-
两个都保护 segment 文件,检查他们的 startTxid,如果不相等,这不合逻辑,抛异常
-
如果都存在 segment 则比较他们的状态,Finalizer 优先于 InProgress,因为 finalized 代表最新的
-
如果两个 segment 都是 finalized,则检查他们的长度是否一致,不一致也是不正常的,因为 finalized 是不会变的,长度应该一样。一样的话随便选择一个
-
比较 Epoch,如果 epoch 不一样,则选择最新的 epoch,这里特别注意上面提到的 AcceptedInEpoch 和 lastWriterEpoch 的比较
-
如果 Epoch 相等,则比较 segment 文件长度,选择较长的
客户端发起AcceptRecovery(P2a)
第一阶段完成即根据提案的响应从中选择一个 value,作为发起 accept 请求的提案,选择算法上面已经描述,接下来就发发起 accept 请求。
Journal接受AcceptRecovery请求(P2b)
accept 阶段需要对提案编号 epoch 检查,因为在提案阶段做了承若。
1 接受请求后的检查 segment 是否包含事务
2 接受请求后的检查上一次 paxos 是否正常完成,这里的检查是判断是否需要去同步数据
检查是否存在上次没有恢复完成的数据,即上轮 paxos 失败了,又发起了新的恢复这里是检查上轮 paxos 实例是否做完,正确退出;如果没有正常退出,则需要判断提案编号,如果本次 accept 的编号 epoch 小于上轮 paxos 的 epoch,则不对。
currentSegment 是当前 journal 本地的日志段,有两种情况需要从其它的journal node 同步数据
-
currentSegment is null,这种情况是 active namenode 还没有发送日志到该 journal 时就 crash了,而且是一个新的 segment
-
文件存在,但是 segment 的长度和需要恢复的 segment 长度不一致
客户端 恢复成功后,超过半数成功返回,则做 finalize
accept 成功后,做第三阶段,commit,这里是 finalize 操作,对文件进行重命名,以便被 namenode 读取
Journal Node 故障的情况
分布式日志系统,除了正常情况下的逻辑处理,更重要的是怎么容灾,如果超过一半,直接不写,因为 QJM 核心就是过半,但如果只是其中一个出现故障,是可以容忍的。
在其中一个 Journal Node Crash 的情况下, QJM 就不会往该故障的 Journal Node 发送日志流了,并标记 outOfSync 为 true, 在什么时候会重新往该节点发送数据呢?会在写新的日志文件时即 startLogSegment RPC 请求的时候,请求成功后,会检查对应的节点 outOfSync 是否为 true,如果是,则重新标记 false,让其开始接受日志,如果在写日志的过程中,有一个节点临时故障,比如网断,后面又恢复,在写新的日志文件之前, QJM 只是会发心跳给写过程中失败的节点,并带上当前的事务 ID(txid),并不立即开始写,可以想下,如果是立即就写,会出现什么情况? 至少会出现事务断层的现象,因为在出现故障期间的事务都没有写到该节点。
大数据时代,Hadoop培训、大数据培训、培训班,就选光环大数据!
大数据培训、人工智能培训、Python培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服!