从 RocketMQ 支持自动故障转移说起
在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,一组 Broker 中有一个 Master,有零到多个 Slave,Slave 通过同步复制或异步复制方式去同步 Master 的数据。Master/Slave 部署模式,提供了一定的高可用性。
但这样的部署模式有一定缺陷。比如故障转移方面,如果主节点挂了还需要人为手动的进行重启或者切换,无法自动将一个从节点转换为主节点。因此,我们希望能有一个新的多副本架构,去解决这个问题。
RocketMQ 实现高可用多副本架构的关键:基于 Raft 协议的 commitlog 存储库 DLedger
Apache RocketMQ - Version 4.5.0 基于 Raft 协议实现高可用多副本架构
master broker 宕机后失去写消息能力
由于 master broker 同时支持读写,slave broker 只支持读,甚至只有 brokerId = 1 的 broker 才支持消息的读负载(rocketmq 根据 brokerId 来确定主从,brokerId = 0 表示 master,brokerId 非 0 表示 slave)
我们商城现在使用的是 RocketMQ 3.2.6 版本,两个节点(broker a 和 broker c)每个 broker 一主一从
除了一主一从,RocketMQ 中的 broker 还支持其他多种部署方式:
- 单 master 单 slave
- 单 master 多 slave 同步复制
- 单 master 多 slave 异步复制
- 多 master 零 slave
kafka 领导者选举
作为对比我们来看看 Kafka 是怎么做的(淘宝中间件团队在对 Kafka 做过充分 Review 之后用 Java 实现了 RocketMQ)。Kafka 中的副本分为追随者副本(Follower Replica)和领导者副本(Leader Replica),追随者副本不对外提供服务,仅用来备份数据(不提供读,也不提供写)优点是不存在从节点消息延迟的情况,缺点是失去了读操作的横向扩展
Kafka Leader 选举大致思路如下:
Leader 在 zookeeper 上创建一个临时节点,所有 Follower 对此节点注册监听,当 Leader 宕机时 ISR 里的所有 Follower 都尝试创建该节点,而创建成功者(Zookeeper 保证只有一个能创建成功)即是新的Leader,其它 Replica 即为Follower
RocketMQ 最佳实践
RocketMQ architecture
RocketMQ 的相关概念
那什么是 Raft 协议
事情一开始是这样的,有个叫 Lamport 的提出一种分布式共识算法(命名 paxos,一个希腊岛屿,古希腊城邦流行民主选举),由于这个算法原本不好理解,加上表述又不够简洁(但并不妨碍人家拿图灵奖,1990 年提出相关算法,后来被互联网公司应用到许多项目中,2013 年获得图灵奖)
最后有人在 2014 年提出了 一种 raft 协议用来替代 paxos 算法并做了简化或者说是优化,raft 协议共有三种角色:
- 领导者(leader)
- 追随者(follower)
- 和候选人(candidate)
raft 协议将问题拆分为数个子问题来解决:
- 领导者选举(leader election)
- 日志复制(log replication)
- 安全规则(safty)
领导者选举
- 在算法初始化阶段或者当现有的领导者宕机/失联时,follower 将以新的任期编号(term)发起一轮领导者选举。
- 如果一轮选举成功则新的领导者开始工作,反之则视此选举结束,下一轮选举将使用一个新的任期编号。
- 当追随者节点接收领导者心跳超时后会选择自己作为候选人发起一轮新的选举。候选人先为自己投一票然后向其他服务器发起投票请求。
- 每个节点在每个任期编号内只允许一次投票,并且遵循先到先服务原则。
- 如果有候选人收到过半的选票就当选为新的领导者,如果超时仍没有选出新领袖则此任期自动终止,将使用新的任期编号开始下一轮选举。
注意:如果候选人(A)在选举过程中有收到其他节点(B)的心跳消息,且心跳包含的任期编号不小于 A 的任期编号,那么 A 会立即恢复为追随者状态,并认定 B 为领导者。
两个随机时间
Raft 使用了两个随机时间来简化 split vote 问题,可以降低多服务同时竞选的几率,也降低了两个竞选人得票都不过半而选举失败的几率
- 每个 follower 等待 leader 发送心跳的超时时间是随机的(150 ms -> 300 ms)
- 两个 candidate 同时选举并获得了相同的票数,这时 candidate 将随机推迟一段时间后再向其他节点重新发出投票请求
Raft 协议特点:
- 系统只存在一个 Leader 角色,接受 Clients 发过来的所有读写请求
- Leader 负责与所有的 Followers 通信,将提案/Value/变更复制到所有 Followers,同时收集多数派 Followers 的应答
- 少数派宕机,不会影响系统整体的可用性
- Leader 日常维护与所有 Followers 的心跳
- Leader 宕机,会触发系统自动重新选主,选主期间系统对外不可服务
日志复制
领导者接收到客户端的请求(包含一个执行的命令)后负责复制日志。领导者收到请求后创建一个新日志项,并附加到本地日志中,然后将新的日志项复制到其他追随者节点。如果追随者节点不可用,则领导者会无限期地重试发送添加日志项消息,直到所有追随者最终接收并存储为止。
当领导者从多数追随者那收到该日志项已被复制的确认消息后,领导者会将该日志项在本地提交。接下来领导者向追随者发送日志项提交消息,通知追随者将日志项应用于其本地状态机。这样也就完成了集群服务器间的日志一致性。
在领导者崩溃的情况下日志可能会不一致,也就是旧领导者的某些日志没在集群中完全复制。新领导者通过强制追随者复制自己的日志来处理不一致问题。大致过程是领导者将每个追随者的日志与自己的日志进行比较,找到跟随者节点上与自己日志相同的最大日志项,然后删除追随者日志中此关键日志项之后的所有日志(此前的日志是完全一致的了)。通过这种机制来恢复故障集群的日志一致性。
看动画理解 Raft 算法
相关算法的工程实践
项目 | 算法 | 时间 |
---|---|---|
mongodb | bully -> raft | 2007 - 2009 开源 |
zookeeper | zab(multi-paxos) | Yahoo 2008 |
chubby | paxos | |
mysql 5.7 group replication | paxos | 2013 - 2015 |
rocketmq | null -> raft | 2012 年开源 |
kafka | zookeeper -> raft | Linked 2011 年开源 |
etcd | raft | CoreOS 2018 年 CNCF |
大致技术的趋势是:master/slave -> raft,paxos -> raft
使用 Raft 协议的中间件
Kafka 在讨论用 Raft 算法替换 zookeeper
MySQL 5.7 Group Replication Background
RocketMQ Add store with dledger
有 raft 之前先有 paxos
Paxos是一种算法,用于在通过异步网络进行通信的一组分布式计算机之间达成共识。一个或多个客户端向 Paxos 提出了一个提议值,当大多数运行 Paxos 的系统都同意其中一个提议值时,将达成共识。Paxos 被广泛使用并且在计算机科学领域具有传奇色彩,因为它是第一个被严格证明是正确的共识算法。
Paxos 只需从提议的一个或多个值中选择一个值,然后让所有人知道该值是什么。如果需要使用 Paxos 创建复制日志(例如,复制状态机),则需要重复运行 Paxos。这称为 multi-Paxos。对于多 multi-Paxos,可以实现一些优化,但是这里不再讨论。
相关角色
Paxos 有三个角色:
- 提议者:接收来自客户的请求(值),并尝试说服接受者接受其提议的值。
- 接受者:接受提议者的某些提议值,并让提议者知道之前是否接受了其他提议。接受者的回复表示对特定提案的投票。
- 学习者:保存备份达成共识的投票结果。
Basic Paxos 协议
该协议是 Paxos 系列中最基础的协议,Basic Paxos 协议成功达成共识后会得到一个提议值。如果一轮协商不成功,通常会进行了几轮协商,一轮成功协商的过程有两个阶段:阶段1(分为 a 和 b )和阶段2(分为 a 和 b )。
第一阶段
阶段 1a: 准备(prepare)
一个提议者创建了一个消息,我们称之为“准备”,用数字 n 标识。请注意, n 不是要提议的值或要商定的值,而只是一个数字,该数字由提议者唯一标识此初始消息(发送给接受者)。数字 n 必须大于此提议者在先前的任何 Prepare 消息中使用过的数字。然后,它发送包含消息 n 的 Prepare 消息给接受者。请注意, Prepare 消息仅包含数字 n (也就是说,它不必包含建议值,通常用 v 表示)。如果提议者不能与任意一个接受者进行通信,则他不应启动 Paxos 协商流程。
阶段 1b: 承诺(promise)
任何 接受者 在等待来自任何 提议者 的 prepare 消息时,如果接受方收到一条 prepare 消息,则接受方必须查看刚刚收到的 prepare 消息的提案编号 n ,此时有两种情况。
如果 n 大于接受者此前从任意提议者处接收到的提案编号,则接受者必须向提议者返回一条消息,我们称其为“承诺”,以忽略将来所有的提案编号小于 n 的提议请求。如果接受者过去某个时候 accepted 了提案,则它必须在对提案者的响应中附带上先前的提案编号(例如 m )和相应的接受值(例如 w )。
否则(即: n 小于或等于接受者收到过的先前提议编号),接受者可以忽略收到的提议。在这种情况下,接受者可以不响应提议者的请求。但是,为了优化起见,发送一个拒绝( Nack )响应将告诉提议者它可以停止使用提案编号 n 来尝试建立提议共识了。
第二阶段
阶段 2a: 提议/接受(propose/accept)
如果提议者从接受者中获得了大部分承诺,则需要为其提案设置提案值 v 。如果接受者先前已接受过提案值,那么他们会将其值发送给提议者,该提议者现在必须将其提案值 v 设置为与接受者响应的最高提案编号相关联的值,我们称之为 z 。如果到目前为止,没有一个接受者 accepted 过提案值,则提议者可以选择任意提案值(例如 x )。
提议者将 accept 消息 [n,v] 发送给接受者,提案编号为 n (与先前发送给提议者的 prepare 消息中包含的编号相同),提案值为 v ( v = z 或者 v = x )。
该 accept 消息应解释为“请求”,类似于“请接受此提议!”。
阶段 2b: 已接受(accepted)
如果一个接受者接收到 accept 消息 [n,v] ,那么只要 n 大于等于接受者已承诺过(在的协议的 1b 阶段)的最大提案编号 maxN,就必须接受这个提议
如果接受者尚未承诺过(在1b 阶段中)提案编号大于 n 提议,则应将刚收到的 accept 消息的值 v 登记为的接受的提案值,并作为已通过的提案值发送给提议者和每个学习者。
否则,它可以忽略这条 accept 消息或请求。
请注意,接受者可以接受多个提议。例如,当一个新的提议者不知道正在协商的原有提案值,以较大的提案编号 n 开始新的一轮提议时,即使接受者之前已接受了一个提案值,它也可以承诺并稍后接受新的提案值。存在某些故障的情况下,这些提议甚至可能具有不同的提案值。但是,Paxos 协议将保证接受者最终会就单个值达成一致。
talk is cheap show me the code
阶段1a:提案人(prepare)
提议者启动 prepare 消息,选择一个唯一的,不断增加的值。
ID 等同于上文的 n,Value 等同于上文的 v
ID = cnt++;
send PREPARE(ID)
阶段1b:接受者(promoise)
接受者收到 prepare[n, ] 消息:
if (ID <= max_id)
do not respond (or respond with a "fail" message)
else
max_id = ID // save highest ID we've seen so far
if (proposal_accepted == true) // was a proposal already accepted?
respond: PROMISE(ID, accepted_ID, accepted_VALUE)
else
respond: PROMISE(ID)
第2a阶段:提案人(propose)
现在,提议者检查是否可以使用其提议,或者是否必须使用从所有响应中收到的编号最高的提议:
did I receive PROMISE responses from a majority of acceptors?
if yes
do any responses contain accepted values (from other proposals)?
if yes
val = accepted_VALUE //value in PROMISE with the highest acceptedID
if no
val = VALUE // we can use our proposed value
send PROPOSE(ID, val) to at least a majority of acceptors
阶段2b:接受者(accepted)
每个接受者都从提议者接收一条PROPOSE(ID,VALUE)消息。如果 ID 是已处理的最大提案编号,则接受该提议并将该值传播给提议者和所有学习者。
if (ID == max_id) // is the ID the largest I have seen so far?
proposal_accepted = true // note that we accepted a proposal
accepted_ID = ID // save the accepted proposal number
accepted_VALUE = VALUE // save the accepted proposal data
respond: ACCEPTED(ID, VALUE) to the proposer and all learners
else
do not respond (or respond with a "fail" message)
如果大多数接受者接受提议,则可以达成共识。注意:达成共识的是提案值 value,而不是提案 ID。
看视频学习 paxos
斯坦福 Paxos lecture
视频部分截图的中文翻译
可以通过下图来帮助理解
图解分布式一致性协议Paxos
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。