目录
集群中Raft算法中各个角色通过RPC通信,主要使用的协议有2种。
一、AppendEntries RPC : 用于Leader节点复制日志给其他节点,也作为心跳。
二、RequestVote RPC: 用于Candidate获取选票。
概述
Raft是一种解决分布式系统一致性问题的算法,旨在替代Paxos。因为Paxos晦涩难懂,所以Raft进行了问题分解,分成了四个子问题,子问题分别是:如何选主(leader election)、如何进行日志复制(log replication)、安全性保证(safety)以及维护成员变化(membership changes),它通过逻辑分离比Paxos更容易理解,但它也被正式证明是安全的,并提供了一些额外的功能。这些一致性协议可以保证在集群中大部分节点(半数以上节点)可用的情况下,集群依然可以工作并给出一个正确的结果。
复制状态机概述
一致性算法是在复制状态机(Replicated State Machine)的背景下提出来的。在这个方法中,一组 Server 的状态机计算相同状态的副本,并且即使有一部分 Server 宕机了它们仍然能够继续运行。
如图:复制状态机架构。一致性算法管理来自客户端的状态机命令的复制日志。状态机处理日志中相同顺序的命令序列,因此会输出相同的结果。
如图所示,一般通过使用复制日志来实现复制状态机。每个 Server 存储着一份包含命令序列的日志文件,状态机会按顺序执行这些命令。因为每个日志包含相同的命令,并且顺序也相同,所以每个状态机处理相同的命令序列。由于状态机是确定性的,所以处理相同的状态,得到相同的输出。
保证复制日志的一致性是一致性算法的任务。一个 Server 上的一致性模块会接收来自客户端的命令,并把命令添加到它的日志文件中。它同其它 Server 上的一致性模块进行通信,确保每一个日志最终包含相同的请求且顺序也相同,即使某些 Server 故障。一旦这些命令被正确复制,每个 Server 的状态机都会按照日志中的顺序去处理,将输出结果返回给客户端。最终,这些Server 看起来就像一个单独的、高可靠的状态机。
一致性算法概述
传统的单体架构的系统对于一致性问题是比较好解决的,数据的写入和读取都在同一个节点上面,要么成功,要么失败。
但是对于分布式架构的系统,数据的写入和读取通常不在同一个节点上面,那么给一致性问题上面带来了挑战。
分布式系统中,网络不可靠,主机的差异性(包块性能、时钟),主机的不可靠等特性,从而产生了分布式系统的一致性问题,我们要保障分布式中的主机以同样的顺序来执行指令,从而产生一致性的结果,使得整个分布式系统像一台主机。
一致性问题
分布式系统产生一致性问题的原因可总结如下:
- 网络不可靠
- 主机不可靠
- 主机之间的差异性(性能、时钟等)
首先,网络可能导致我们发送的数据或者指令以乱序的方式到达,也可能会丢失数据,其次主机时不可靠的,可能会出现宕机,重启等,主机之间的差异性,包括主机的性能和时钟,这些都会导致分布式系统难于实现一致性,FLP不可能理论指出无法彻底解决一致性问题,在CAP中我们只能选择两项,大多数的分布式系统都选择了最终一致性,即若一致性来保证可用性和分区容错性。
对于实际系统,一致性算法一般具有如下特点:
- 安全。满足在所有非拜占庭条件下确保安全(从来不会返回错误结果),包括网络延迟、分区、丢包、重复和重排序。
- 高可用。只要集群中的大部分 Server 正常运行,并能够互相通信且可以同客户端通信,这个集群就完全可用。因此,拥有5个 Server 的集群可以容忍其中的2个 Server 失败。假使通过停掉某些 Server 使其失败,稍后它们会从持久化存储的状态进行恢复,并重新加入到集群中。
- 不依赖于时序。确保日志的一致性:时钟错误,以及极端情况下的消息延迟,在最坏的情况下都会造成可用性问题。
- 通常情况下,只要集群中大多数 Server 成功响应了某一轮 RPC 调用,一个命令就算完成。少部分较慢的 Server 不应该影响到整个系统性能。
通俗来讲,一致性的问题可以分解为两个问题:
- 任何一次修改保证数据一致性
- 多次数据修改的一致性
弱一致性:不要求每次修改的内容在修改后多副本的内容是一致的,对问题1的解决比较宽松,更多解决问题2,该类算法追求每次修改的高度并发性,减少多副本之间修改的关联性,以获得更好的并发性能。例如最终一致性,无所谓每次用户修改后的多副本的一致性及格过,只要求在单调的时间方向上,数据最终保持一致,如此获得了修改极大的并发性能。
强一致性:强调单次修改后结果的一致,需要保证了对问题1和问题2要求的实现,牺牲了并发性能。
一致性算法有:两阶段提交算法、分布式锁服务、Paxos算法和Raft算法。
下面主要介绍Raft一致性算法。
Raft一致性算法概述
Raft算法由三种角色组成
角色身份,又叫做服务器节点状态,领导者(Leader)、跟随者(Follower)和候选者(Candidate) 3 种状态。
- 领导者:所有客户端请求的处理者,平常的主要工作内容就是 3 部分,处理写请求、管理日志复制和不断地发送心跳信息。
- 候选者:候选人将向其他节点发送请求投票(RequestVote)RPC 消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
- 跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,然后写入本地日志文件。当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
Raft算法另一个很重要的概念任期
- Raft 把时间分割成任意长度的任期(term),如图 5 所示。任期用连续的整数标记。
- 每一段任期从一次选举开始,一个或者多个候选者尝试成为领导者。如果一个候选者赢得选举,然后他就在该任期剩下的时间里充当领导者。在某些情况下,一次选举无法选出领导者。在这种情况下,这一任期会以没有领导者结束;一个新的任期(包含一次新的选举)会很快重新开始。
- Raft 保证了在任意一个任期内,最多只有一个领导者。
任期的作用:
- 不同的服务器节点观察到的任期转换的次数可能不同,在某些情况下,一个服务器节点可能没有看到领导者选举过程或者甚至整个任期全程。
- 任期在 Raft 算法中充当逻辑时钟的作用,这使得服务器节点可以发现一些过期的信息比如过时的领导者。
- 每一个服务器节点存储一个当前任期号,该编号随着时间单调递增。
- 服务器之间通信的时候会交换当前任期号;
- 如果一个服务器的当前任期号比其他的小,该服务器会将自己的任期号更新为较大的那个值。
- 如果一个候选者或者领导者发现自己的任期号过期了,它会立即回到 跟随者状态。(所以说老领导者如果发生了网络分区,后来接收到新领导者的心跳的时候,比拼完任期之后,会自动变成跟随者。
- 如果一个节点接收到一个包含过期的任期号的请求,它会直接拒绝这个请求。
Raft算法中各个角色的状态信息表
状态 | 所有节点上持久化的状态(在响应RPC请求之前变更且持久化的状态) |
currentTerm | 服务器的任期,初始为0,递增 |
votedFor | 在当前获得选票的候选人的 Id |
log[] | 日志条目集;每一个条目包含一个用户状态机执行的指令,和收到时的任期号 |
状态 | 所有节点上非持久化的状态 |
commitIndex | 最大的已经被commit的日志的index |
lastApplied | 最大的已经被应用到状态机的index |
状态 | Leader节点上非持久化的状态(选举后重新初始化) |
nextIndex[] | 每个节点下一次应该接收的日志的index(初始化为Leader节点最后一个日志的Index + 1) |
matchIndex[] | 每个节点已经复制的日志的最大的索引(初始化为0,之后递增) |
集群中Raft算法中各个角色通过RPC通信,主要使用的协议有2种。
一、AppendEntries RPC : 用于Leader节点复制日志给其他节点,也作为心跳。
参数 | 解释 |
term | Leader节点的任期 |
leaderId | Leader节点的ID |
prevLogIndex | 此次追加请求的上一个日志的索引 |
prevLogTerm | 此次追加请求的上一个日志的任期 |
entries[] | 追加的日志(空则为心跳请求) |
leaderCommit | Leader上已经Commit的Index |
prevLogIndex和prevLogTerm表示上一次发送的日志的索引和任期,用于保证收到的日志是连续的。
返回值 | 解释 |
term | 当前任期号,用于Leader节点更新自己的任期(应该说是如果这个返回值比Leader自身的任期大,那么Leader需要更新自己的任期) |
success | 如何Follower节点匹配prevLogIndex和prevLogTerm,返回true |
接收者实现逻辑
- 如果收到的任期比当前任期小, 返回false
- 如果不包含之前的日志条目(没有匹配prevLogIndex和prevLogTerm), 返回false
- 如果存在index相同但是term不相同的日志,删除从该位置开始所有的日志
- 追加所有不存在的日志
- 如果leaderCommit>commitIndex,将commitIndex设置为commitIndex = min(leaderCommit, index of last new entry)
二、RequestVote RPC: 用于Candidate获取选票。
参数 | 解释 |
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最后一条日志的索引 |
lastLogTerm | Candidate最后一条日志的任期 |
参数 | 解释 |
term | 当前任期,用于Candidate更新自己的任期 |
voteGranted | true表示给Candidate投票 |
接收者的实现逻辑
- 如果收到的任期比当前任期小, 返回false
- 如果本地状态中votedFor为null或者candidateId,且candidate的日志等于或多余(按照index判断)接收者的日志,则接收者投票给candidate,即返回true
Raft算法之选主流程
Raft使用心跳协议来触发选主,当集群内节点启动时,状态是跟随者。当节点从领导者或者候选者接收到合法的RPC时,它会保持在跟随者状态。领导者会发送周期性的心跳来表明自己是领导者。
当一个follower在election timeout(通常是在150ms-300ms之间没有接收到leader的心跳信息)时间内没有接收到通信,那么它会开始选主。
选主的步骤如下:
- 增加current term
- 转成candidate状态
- 选自己为主,然后把选主RequestVote RPC并行地发送给其他的节点
- candidate状态会继续保持,直到下述三种情况出现
4.1 它赢得了选举;
4.2 另一台服务器赢得了选举;
4.3 一段时间后没有任何一台服务器赢得了选举。
这些情形会在下面的图中分别讨论。
一、情况一 候选者自己赢得选举
二、情况二 另一台服务器赢得了选举
三、情况三 一段时间后没有任何一台服务器赢得了选举
网络分区产生的处理机制如下:
在发生网络分区的时候,Raft一样能保持一致性。如下图所示,假设我们的集群由5个节点组成,且节点B是Leader节点:
我们假设发生了网络分区:节点A和B在一个网络分区,节点C、D和E在另一个网络分区,如下图所示,且节点B和节点C分别是两个网络分区中的Leader节点:
我们假设还有一个客户端,并且往节点B上发送了一个SET 3,由于网络分区的原因,这个值不能被另一个网络分区中的Leader即节点C拿到,它最多只能被两个节点(节点B和C)感知到,所以它的状态是uncomitted(红色):
另一个客户端准备执行SET 8的操作,由于可以被同一个分区下总计三个节点(节点C、D和E)感知到,3个节点已经符合大多数节点的条件。所以,这个值的状态就是committed:
接下来,我们假设网络恢复正常,如下图所示。节点B能感知到C节点这个Leader的存在,它就会从Leader状态退回到Follower状态,并且节点A和B会回滚之前没有提交的日志(SET 3产生的uncommitted日志)。同时,节点A和B会从新的Leader节点即C节点获取最新的日志(SET 8产生的日志),从而将它们的值更新为8。如此以来,整个集群的5个节点数据完全一致了:
Raft算法之日志概念
日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),还包含一些附加信息,比如索引值(Log index)、任期编号(Term)。那该怎么理解这些信息呢?
- 指令:一条由客户端请求指定的、状态机需要执行的指令。你可以将指令理解成客户端指定的数据。
- 索引值:日志项对应的整数索引值。它其实就是用来标识日志项的,是一个连续的、单调递增的整数号码。
- 任期编号:创建这条日志项的领导者的任期编号。
从图中可以看到,一届领导者任期,往往有多条日志项。而且日志项的索引值是连续的。
可以看到上图中4个跟随者的日志都不一样?日志是怎么复制的呢?又该如何实现日志的一致呢?下文我们来讨论这几个问题。先来说说如何复制日志。
Raft算法之日志复制流程
为了帮助理解,我画了几张过程图,然后再带你走一遍这个过程,这样你可以更加全面地掌握日志复制。
- 接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
- 领导者通过日志复制 RPC,将新的日志项复制到其他的服务器。
- 当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。
- 领导者将执行的结果返回给客户端。
- 当跟随者接收到心跳信息,或者新的日志复制 RPC 消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机中。
不过,这是一个理想状态下的日志复制过程。在实际环境中,复制日志的时候,你可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致。那么在这种情况下,Raft 算法是如何处理不一致日志,实现日志的一致的呢?
日志的一致性保证。
在Raft协议中有两个主要的消息,一个是在第二节讲到的RequestVote RPC,用于选主投票时leader发出的消息。一个就是AppendEntries RPC,用于心跳和日志复制。对于心跳,只需要发送空内容的AppendEntries RPC就可以了,我们主要关注日志复制的消息,看看Raft是怎么操作的。
- leader接受客户端的操作请求,如“X = 3”。假如leader当前的任期为termY,那么leader就会向自己本地log的索引K的位置添加一个log entry:“termY:X = 3”。之所以添加到K是因为K索引位置之前已经有了log内容了。
- leader向集群中其他follower并行发送AppendEntries RPC消息。协议中数据由两部分组成。
<1> 这个新的log entry:“termY:X = 3”。
<2> K-1,以及K-1索引位置的log entry。 - 当一个follower收到一个AppendEntries RPC消息时会查看自己本地的log中的K-1位置的entry的内容。
- 假如本地log中K-1位置的entry内容与接收到的来自leader的K-1的entry内容一致,那么就将leader发来的K位置的entry保存在自己的K位置,并返回true,告诉leader保存成功了。
- 假如本地log中K-1位置的entry内容与接收到的来自leader的K-1的entry内容不一致,那么就返回false,告诉leader不一致。leader收到消息后,会将K减小一点,然后再次重新发AppendEntries RPC,直到follower返回true。因为到K=0的时候肯定会一样(都没有),因此早晚一定会得到true的回复。此时leader将匹配的位置和最新的位置中间的内容都发送给follower,follower会将接收到的内容覆盖到对应的位置。
4. 经过第3步的操作,此时follower和leader的日志就已经一样了。
当leader收到了大多数的follower的true的返回,那么leader就可以回复客户端,已经成功更新了数据。
Raft算法之安全性
一、选举限制
在Raft协议中,所有的日志条目都只会从Leader节点往Follower节点写入,且Leader节点上的日志只会增加,绝对不会删除或者覆盖。
这意味着Leader节点必须包含所有已经提交的日志,即能被选举为Leader的节点一定需要包含所有的已经提交的日志。因为日志只会从Leader向Follower传输,所以如果被选举出的Leader缺少已经Commit的日志,那么这些已经提交的日志就会丢失,显然这是不符合要求的。
这就是Leader选举的限制:能被选举成为Leader的节点,一定包含了所有已经提交的日志条目。
回看算法基础中的RequestVote RPC:
参数 | 解释 |
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最后一条日志的索引 |
lastLogTerm | Candidate最后一条日志的任期 |
参数 | 解释 |
term | 当前任期,用于Candidate更新自己的任期 |
voteGranted | true表示给Candidate投票 |
请求中的lastLogIndex和lastLogTerm即用于保证Follower投票选出的Leader一定包含了已经被提交的所有日志条目。
- Candidate需要收到超过版本的节点的选票来成为Leader
- 已经提交的日志条目至少存在于超过半数的节点上
- 那么这两个集合一定存在交集(至少一个节点),且Follower只会投票给日志条目比自己的“新”的Candidate,那么被选出的节点的日志一定包含了交集中的节点已经Commit的日志
日志比较规则(即上面“新”的含义):Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。
二、日志提交限制
上图按时间序列展示了Leader在提交日志时可能会遇到的问题。
- 在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。
- 在 (b) 中,S1 崩溃了,然后 S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。
- 然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。
- 如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。反之,如果在崩溃之前,S1 把自己主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(因为S5 就不可能选举成功)。这样在同一时刻就同时保证了,之前的所有老的日志条目就会被提交。
任期2内产生的日志可能在(d)的情况下被覆盖,所以在出现(c)的状态下,Leader节点是不能commit任期2的日志条目的,即不能更新commitIndex。
在上图最终状态是(e)的情况下,commitIndex的变化应该是1->3,即在(c)的情况下,任期4在索引3的位置commit了一条消息,commitIndex直接被修改成3。
而任期2的那条日志会通过Log Matching Property最终被复制到大多数节点企且被应用。
Raft算法保证了以下特性:• 如果两个日志条目有相同的index和term,那么他们存储了相同的指令(即index和term相同,那么可定是同一条指令,就是同一个日志条目) • 如果不同的日志中有两个日志条目,他们的index和term相同,那么这个条目之前的所有日志都相同 两条规则合并起来的含义:两个日志LogA、LogB,如果LogA[i].index=Log[i]B.index且LogA[i].term=Log[i].term,那么LogA[i]=Log[i]B,且对于任何n < i的日志条目,LogA[n]=LogB[n]都成立。(这个结论显而易见的可以从日志复制规则中推导出来)
Raft算法之维护成员变化
在成员变更时,因为无法做到在同一个时刻使所有的节点从旧配置转换到新配置,那么直接从就配置向新配置切换就可能存在一个节点同时满足新旧配置的“超过半数”原则。
如下图,原集群由Server1、Server2、Server3,现在对集群做变更,增加Server4、Server5。如果采用直接从旧配置到新配置的切换,那么有一段时间存在两个不想交的“超过半数的集群”。
上图,中在中间位置Server1可以通过自身和Server2的选票成为Leader(满足旧配置下收到大多数选票的原则);Server3可以通过自身和Server4、Server5的选票成为Leader(满足新配置线,即集群有5个节点的情况下的收到大多数选票的原则);此时整个集群可能在同一任期中出现了两个Leader,这和协议是违背的。
为了保证安全性,Raft采用了一种两阶段的方式。
第一阶段称为joint consensus,当joint consensus被提交后切换到新的配置下。
joint consensus状态下:
-
日志被提交给新老配置下所有的节点
-
新旧配置中所有机器都可能称为Leader
-
达成一致(选举和提交)要在两种配置上获得超过半数的支持
具体的切换过程如下:
-
Leader收到C-old到C-new的配置变更请求时,创建C-old-new的日志并开始复制给其他节点(和普通日志复制没有区别)
-
Follower以最新的配置做决定(收到C-old-new后就以C-old-new来决定),Leader需要以已经提交的配置来做决定(即只有C-old-new复制到大多数节点后Leader才以这个配置做决定);这个时候处于一个共同决定的过程
-
之后提交C-new到所有节点,一旦C-new被提交,旧的配置就无所谓了
从上图可以看出,不存在一个阶段C-old和C-new可以同时根据自己的配置做出决定。
结论
到此Raft一致性算法就算讲解完毕了,虽然说Raft算法的产生是为了让人更加容易理解分布式一致性算法,不像Paxos算法那么难于理解,但是通过上文也可以看出,Raft算法也没有想象的那么简单,相对应的细节也很多。可能有些细节也没有在文中描述出来,在以后的工作中会逐步完善。现在Raft被广泛的用于开源框架中,比如百度的braft,CockroachDB数据库,Tidb数据库,etcd一个分布式、可靠 key-value存储的分布式系统等等。所以对于Raft算法有基本的了解还是有必要的。
References
- · Distributed Consensus with Raft – CodeConf 2016 – GitHub – YouTube
- · An Introduction to Raft (CoreOS Fest 2015) – CoreOS – YouTube
- · In search of an understandable Consensus Algorithm – Raft – Extended Version
- · Medium article – Understanding Raft
- · Consensus – Wikipedia
- · Rutgers – Consensus
- · Duke University – Consensus
- · CAP theorem – Medium
- · Container Solutions – raft explained 1/3
- · Container Solutions – raft explained 2/3
- · Container Solutions – Raft explained – 1/3
- · Consul consensus protocol
- · University of Cambridge – Analysis of Raft by Heidi Howard
- · Is Raft as good as Zab
- · Heidi Howard Raft GitHub