目录
Raft算法
Raft算法属于Multi-Paxos算法,它是在Multi-Paxos思想的基础上,做了一些简化和限制,比如增加了日志必须是连续的,只支持领导者、跟随者和候选人三种状态。**Raft算法是现在分布式系统开发首选的共识算法。**绝大多数选用Paxos算法的系统(如Cubby、Spanner)都是在Raft算法发布之前开发的;全新的系统大多选择了Raft算法(如Etcd、Consul、CockroachDB)。
从本质上说,Raft算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致。
下面从领导者选举、日志复制、成员变更介绍Raft算法的原理。
💡💡💡
领导者选举、日志复制、成员变更是Raft算法的重要部分。
领导者选举
在一个有多个节点的集群中,在节点故障、分区错误等异常的情况下,Raft算法如何保证在统一时间,集群中只有一个领导者呢?
有哪些成员身份?
成员身份,又叫做服务器节点状态,Raft算法支持领导者(Leader)、跟随者(Follower)、和候选人(Candidate)3中状态。
跟随者:相当于普通群众,接受和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
候选人:候选人向其他节点发送请求投票(RequestVote)RPC消息,通知其他节点来投票,如果赢了大多数选票,就晋升当领导者。
领导者:一切以领导者为准,工作内容有3部分:处理写请求 、管理日志复制 和不断地发送心跳信息。其中不断发送心跳信息是为了告诉其他节点领导者还活着,不要发起新的选举。
‼️
Raft算法是强领导者模型,集群中只能有一个领导者。
领导者选举流程
首先,在初始状态下,集群中所有的节点都是跟随者的状态。
Raft 算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。等待超时时间最小的节点会最先因为没有等到领导者的心跳信息而发生超时(节点A)。此时节点 A 就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票 RPC 消息,请它们选举自己为领导者。
如果其他节点接收到候选人 A 的请求投票 RPC 消息,在编号为 1 的这届任期内,也还没有进行过投票,那么它将把选票投给节点 A,并增加自己的任期编号(与节点A一致)。
🤔️🤔️🤔️
如出现其他节点投过票了,或者其他节点任期编号比A的任期编号大等情况又会怎么做?
在下面的选举细节中介绍。
如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。节点 A 当选领导者后,它将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举,篡权。
🤔️🤔️🤔️
以上选举过程为集群启动初始状态下的选举,而节点的增加、减少又会出现怎样的情况?
会在后面的成员变更一节中进行介绍。
以上是简单的选举过程,但其中还有一些细节,比如:
- 节点之间如何通信的?
- 什么是任期?
- 选举有哪些规则?
- 随机超时时间是什么?
选举细节
节点之间如何通信
Raft算法中,服务器节点之间的联络采用的是远程过程调用(RPC),在领导者选举中,需要用到以下两类RPC:
1、请求投票(RequestVote)RPC,由候选人在选举期间发起,通知各节点进行投票。
2、日志复制(AppendEntries)RPC,由领导者发起,用来复制日志和提供心跳信息。
日志复制 RPC 只能由领导者发起,这是实现强领导者模型的关键之一。
其他节点之间如何通信?我感觉它们之间不用通信。
什么是任期
Raft算法中的领导者是有任期的,每个任期由单调递增的数字表示。任期编号是随着选举的举行而变化的:
- **跟随者在等待领导者心跳信息超时后,推荐自己为候选人时,会增加自己的任期号。**比如节点 A 的当前任期编号为 0,那么在推举自己为候选人时,会将自己的任期编号增加为 1。
- **如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。**比如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1。
此外,Raft算法中还做了如下约定:
- **如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。**比如分区错误恢复后,任期编号为 3 的领导者节点 B,收到来自新领导者的,包含任期编号为 4 的心跳消息,那么节点 B 将立即恢复成跟随者状态。
- **如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。**比如节点 C 的任期编号为 4,收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息。
选举有哪写规则
在 Raft 算法中,也约定了选举规则,主要有这样几点:
- 领导者周期性地向所有跟随者发送心跳消息(即不包含日志项的日志复制 RPC 消息),通知大家我是领导者,阻止跟随者发起新的选举。
- 如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。
- 在一次选举中,赢得大多数选票的候选人,将晋升为领导者。
- 在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其他节点发起一轮新的选举。
- 在一次选举中,每一个服务器节点最多会对一个任期编号投出一张选票,并且按照"先来先服务"的原则进行投票。比如节点 C 的任期编号为 3,先收到了 1 个包含任期编号为 4 的投票请求(来自节点 A),然后又收到了 1 个包含任期编号为 4 的投票请求(来自节点 B)。那么节点 C 将会把唯一一张选票投给节点 A,当再收到节点 B 的投票请求 RPC 消息时,对于编号为 4 的任期,已没有选票可投了。
- 日志完整性高的跟随者(也就是最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点 B 的任期编号为 3,节点 C 的任期编号是 4,节点 B 的最后一条日志项对应的任期编号为 3,而节点 C 为 2,那么当节点 C 请求节点 B 投票给自己时,节点 B 将拒绝投票。
选举是跟随者发起的,推荐自己为候选人;
大多数选票是指集群成员半数以上的选票;
大多数选票规则的目标,是为了保证在一个给定的任期内最多只有一个领导者;
随机超时时间是什么
在选举过程中会有以下选举失败的情况,比如同一任期内,多个候选人同时发起选举,导致选票被瓜分,选举失败。Raft如何避免这种情况呢?
在Raft算法中使用随机超时时间的方法,把超时时间都分散开,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分而导致选举失败的情况了。
在Raft算法中,随机超时时间由2中含义:
1、跟随者等待领导者心跳信息超时的时间间隔,是随机的。
2、如果候选人在一个随机时间间隔内,没有赢得过半票数,那么选举无效。然后候选人发起新一轮的选举,也就是说等待选举超时的时间间隔,也是随机的。
小结
- Raft 算法和兰伯特的 Multi-Paxos 不同之处,主要有 2 点。首先,在 Raft 中,不是所有节点都能当选领导者,只有日志较完整的节点(也就是日志完整度不比半数节点低的节点),才能当选领导者;其次,在 Raft 中,日志必须是连续的。
- Raft 算法通过任期、领导者心跳消息、随机选举超时时间、先来先服务的投票原则、大多数选票原则等,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。
- 本质上,Raft 算法以领导者为中心,选举出的领导者,以"一切以我为准"的方式,达成值的共识,和实现各节点日志的一致。
日志复制
在选完领导者之后,领导者需要处理来自客户端的写请求,并通过日志复制实现各节点日志的一致。
在Raft算法中,数据副本是以日志的形式存在的,领导者接受到来自客户端的写请求后,处理写请求的过程就是一个复制和应用(Apply)日志项到状态机的过程。
💡💡💡
领导者将客户端的请求以日志的形式发送给其他节点,其中日志中包含执行的命令(如达成共识的值)。
下面看看Raft算法是如何复制日志,又如何实现日志的一致的。
如何理解日志
副本数据是以日志的形式存在的,日志是由日志项组成。日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),还包含一些附加信息,比如索引值(Log index)、任期编号(Term)
- 指令:一条由客户端请求指定的、状态机需要执行的指令。可以将指令理解成客户端指定的数据。
- 索引值:日志项对应的整数索引值。它其实就是用来标识日志项的,是一个连续的、单调递增的整数号码。
- 任期编号:创建这条日志项的领导者的任期编号。
如何复制日志
Raft的日志复制是一个优化后的二阶段提交(将二阶段优化成一阶段),减少了一半的往返消息,也就是降低了一半的消息延迟。具体的复制过程如下:
- 首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC 消息,将日志项复制到集群其他节点上。
- 如果领导者接收到大多数的"复制成功"响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的"复制成功"响应,那么就返回错误给客户端。
❓❓❓
领导者将日志项应用到它的状态机,怎么没通知跟随者应用日志项呢?
其实这是Raft中的一个优化,领导者不直接发送消息通知其他节点应用指定日志项。因为领导者的日志复制 RPC 消息或心跳消息,包含了当前最大的,将会被提交(Commit)的日志项索引值。所以可以通过后续的日志复制RPC消息或者心跳消息,跟随者就可以知道领导者的日志提交位置,进而应用日志项中的指令。这个优化,降低了处理客户端请求的延迟,将二阶段提交优化为了一段提交,降低了一半的消息延迟。
下述是日志复制的详细流程:
- 接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
- 领导者通过日志复制 RPC,将新的日志项复制到其他的服务器。
- 当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。(如果没有复制成功,会进行重试,最终也没有复制成功的日志项不会应用到状态机)。
- 领导者将执行的结果返回给客户端。
- 当跟随者接收到心跳信息,或者新的日志复制 RPC 消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机中。
如何实现日志的一致
在实际的环境中,复制日志的时候,可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致,那么在这种情况下Raft算法如何处理不一致日志,实现日志的一致呢?
在Raft算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致的日志。Raft是通过以领导者的日志为准,来实现各节点的日志一致的,具体有2个步骤:
- 首先,领导者通过日志复制 RPC 的一致性检查,找到跟随者节点上,与自己相同日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了。
- 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。
小结
- 在 Raft 中,副本数据是以日志的形式存在的,其中日志项中的指令表示用户指定的数据。
- 在 Raft 中,副本数据是以日志的形式存在的,其中日志项中的指令表示用户指定的数据。
- Raft 是通过以领导者的日志为准,来实现日志的一致的。
节点成员变更
相关术语:
什么是网络分区:
分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题),某些节点之间不连通了,整个网络就分成了几块区域,就叫网络分区。
什么是配置:
可以理解成:集群是哪些节点组成的,是集群节点地址信息的集合。比如节点A、B、C组成的集群,那么集群的配置就是[A、B、C]集合。
当遇到需要改变数据副本数的情况,则需要增加或移除集群中的服务器进行成员变更。在对集群成员进行变更时可能会出现多个领导者,从而破坏Raft集群的领导者唯一性,影响集群的性能。
Raft算法中最初实现成员变更的是联合共识(Joint Consensus),但是该方法实现比较困难,之后Raft作者就提出了一种改进后的方法:单节点变更(single-server changes)。
成员变更的问题
成员变更问题最大的风险是,可能会同时出现2个领导者。
比如,一开始集群中有A、B、C三个节点,其中节点A是领导者,在成员变更时,节点A、B和C之间发生了分区错误(节点A和B 与节点C失去联系了),节点A、B组成旧配置中的"大多数",也就是变更前的3节点集群中的"大多数",那么这时的领导者(节点A)依旧是领导者;另一方面,节点C和新节点D、E组成了新配置的"大多数",也就是变更后5节点集群中的"大多数",它们可能会选举出新的领导者(如节点C)。这时就同时出现了2个领导者的情况。
这个时候节点A、B的配置是[A、B、C],因此需要满足的大多数是2个节点。而节点C、D、E的配置是[A、B、C、D、E],因此需要满足的大多数是3个节点。
❓❓❓新配置A、B节点没有读取到吗,只有C、D、E知道吗?
是因为A、B和C出现了网络分区,D和E只联系到了C,而没有联系到A和B的原因吧。
可以通过将A、B、C组成的集群关闭,然后再启动节点A、B、C、D、E组成的新集群进行解决。但是每次变更都要重启集群,意味着在集群变更期间服务不可用,这样肯定是不行的,太影响用户体验。在Raft中,使用单节点变更解决这一问题。
如何通过单节点变更解决成员变更的问题
单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。比如将 3 节点集群扩容为 5 节点集群,这时你需要执行 2 次单节点变更,先将 3 节点集群变更为 4 节点集群,然后再将 4 节点集群变更为 5 节点集群。
下面是使用单节点变更将配置为[A(Leader)、B、C]的3节点集群变更为配置为[A、B、C、D、E]的5节点集群的详细流程:
目前的集群配置为[A, B, C],我们先向集群中加入节点 D,这意味着新配置为[A, B, C, D]。成员变更,是通过这么两步实现的:
- 第一步,领导者(节点 A)向新节点(节点 D)同步数据;
- 第二步,领导者(节点 A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点 A、B、C、D)上,然后将新配置的日志项应用(Apply)到本地状态机,完成单节点变更。
在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点 E,也就是说,新配置为[A, B, C, D, E]。成员变更的步骤和上面类似:
- 第一步,领导者(节点 A)向新节点(节点 E)同步数据;
- 第二步,领导者(节点 A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项应用到本地状态机,完成单节点变更。
在大多数情况下,不管旧的集群配置是怎么组成的,旧配置的"大多数"和新配置的"大多数"都会有一个节点是重叠的。也就是说,不会同时存在旧配置和新配置2个"大多数",如下图所示:
不管集群是偶数节点,还是奇数节点,不管是增加节点,还是移除节点,新旧配置的"大多数"都会存在重叠(图中的橙色节点)。
在分区错误、节点故障等情况下,如果我们并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。如果你遇到这种情况,可以在领导者启动时,创建一个 NO_OP 日志项(也就是空日志项),只有当领导者将 NO_OP 日志项应用后,再执行成员变更请求。
小结
- 成员变更的问题,主要在于进行成员变更时,可能存在新旧配置的 2 个"大多数",导致集群中同时出现两个领导者,破坏了 Raft 的领导者的唯一性原则,影响了集群的稳定运行。
- 单节点变更是利用"一次变更一个节点,不会同时存在旧配置和新配置 2 个'大多数'"的特性,实现成员变更。
- 因为联合共识实现起来复杂,不好实现,所以绝大多数 Raft 算法的实现,采用的都是单节点变更的方法(比如 Etcd、Hashicorp Raft)
Raft小结
Raft 不是一致性算法而是共识算法,是一个 Multi-Paxos 算法,实现的是如何就一系列值达成共识。Raft 能容忍少数节点的故障。虽然 Raft 算法能实现强一致性,也就是线性一致性(Linearizability),但需要客户端协议的配合。