首发于ETCD
Raft 理论基础

Raft 理论基础

一、从“拜占庭将军问题”开始

1.1 问题描述

拜占庭将军问题(Byzantine failures),是由莱斯利·兰伯特提出的点对点通信中的基本问题。问题内容如下:

拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了达到防御目的,每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军和副官必须达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。在进行共识时,结果并不代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。

问题:如果这其中有人叛变,传递错误的信息,那么怎么同时进攻来取得胜利呢?

1.2 “拜占庭将军问题” 化简

拜占庭将军问题是分布式领域最复杂、最严格的容错模型。但在日常工作中使用的分布式系统面对的问题不会那么复杂,更多的是计算机故障挂掉了,或者网络通信问题而没法传递信息,这种情况不考虑计算机之间互相发送恶意信息,极大简化了系统对容错的要求,最主要的是达到一致性。

所以将拜占庭将军问题根据常见的工作问题进行化简:假设将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成一致性决定?

1.3 Raft 如何解决简化版“拜占庭将军问题”

对于这个简化后的问题,有许多解决方案,第一个被证明的共识算法是 Paxos,由拜占庭将军问题的作者 Leslie Lamport 在1990年提出,最初以论文难懂而出名,后来这哥们在2001重新发了一篇简单版的论文 Paxos Made Simple,然而还是挺难懂的。

因为 Paxos 难懂,难实现,所以斯坦福大学的教授在2014年发表了新的分布式协议 Raft。与 Paxos 相比,Raft 有着基本相同运行效率,但是更容易理解,也更容易被用在系统开发上。

Raft 解决方案是 在所有将军中选出一个大将军,所有决定由大将军来做。大致流程如下:

  1. 比如说现在一共有3个将军 A, B, C,每个将军都有一个随机时间的倒计时器(假设将军A的随机时间最短)。倒计时一结束,将军A就会把自己当成大将军候选人,然后派信使去问其他几个将军,能不能选我为总将军
  2. 假设现在将军A倒计时结束了,他派信使传递选举投票的信息给将军B和C
  3. 当将军B和C收到信使传递的选举信息后,如果将军B和C还没把自己当成候选人(倒计时还没有结束),并且没有把选举票投给其他人,他们把票投给将军A
  4. 信使回到将军A后,将军A知道自己收到了足够的票数,成为了大将军
  5. 在这之后,是否要进攻就由大将军决定,然后派信使去通知另外两个将军(将军B和C)
  6. 如果在一段时间后还没有收到回复(可能信使被暗杀),那就再重派一个信使,直到收到回复

二、Raft基础

2.1 Raft 定义

首先,Raft是一种“算法”;其次,Raft 是一种为了管理“复制日志”算法;最后,Raft 是一种为了管理复制日志“一致性”算法。

那么问题来了,什么是一致性?

一致性是分布式系统容错的基本问题。一组机器像一个整体一样工作,即使其中小半部分机器(不大于N/2)出现故障也能够继续工作下去, 一旦他们就状态做出决定,该决定就是最终决定。 例如,即使2台服务器发生故障,5台服务器的集群也可以继续运行。 如果更多服务器失败,它们将停止进展(但永远不会返回错误的结果)

2.2 Raft 三种角色

根据 “拜占庭将军问题” ,我们可以提取出三种状态的角色:

1. 追随者:将军B和C愿意投票给将军A, 将军B和C成为将军A的跟随者

2. 候选者:将军A倒计时结束,成为大将军候选者

3. 大将军:将军A收到大多数将军的投票后,成为大将军

在包含若干节点Raft集群中,其实也存在着相似的角色:Leader、Candidate、Follower。每种角色负责的任务也不一样,正常情况下,集群中的节点只存在 Leader 与 Follower 两种角色。

1. Leader(领导者):处理所有客户端交互,日志复制等,一般一次只有一个Leader;

2. Follower(追随者):响应 Leader 的日志同步请求,响应 Candidate 的邀票请求,以及把客户端请求到 Follower 的事务转发(重定向)给 Leader;

3. Candidate(候选者):负责选举投票,集群刚启动或者 Leader 宕机时,角色为 Follower 的节点将转为 Candidate 并发起选举,选举胜出(获得超过半数节点的投票)后,从 Candidate 转为 Leader 角色;

2.3 Raft 算法问题分解

根据上面的介绍,我们知道通常Raft集群中只有一个Leader,其它节点都是Follower。Follower都是被动的:他们不会发送任何请求,只是简单的响应来自Leader或者Candidate的请求。Leader负责处理所有的客户端请求(如果一个客户端和Follower联系,Follower会把请求重定向给Leader)。为了简化逻辑和实现,Raft将一致性问题分解成三个独立的子问题:

1. Leader election:当leader宕机或者集群创建时,需要选举一个新的Leader

2. Log replication:Leader接收来自客户端的请求并将其以日志的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致

3. Safety:如果有任何节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务节点不能在同一个日志索引位置应用一个不用的指令

三、Raft 算法原理

上面讲了这么多,其实都是伏笔 。接下来,本文的核心知识点来了。

3.1 Raft 角色选举

根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点状态都是Follower态,由于没有Leader,Follower 无法与 Leader 保持心跳(heart beat),Follower等待心跳超时(每个Follower的心跳超时时间不一样),Followers 会认为 Leader 已经 down 掉。最先超时的Follower进而转为 Candidate 状态,然后,Candidate 将向集群中的其它节点请求投票,同意自己升级为 Leader,如果 Candidate 收到超过半数节点的投票(N/2+1),它将获胜成为 Leader。

角色选举详细流程如下:

第一阶段:都是 Follower 状态

一个应用Raft协议的集群在刚开始启动时(或者 Leader 宕机重启时),所有的节点都是 Follower 状态,初始任期(Term,即某次选举的唯一标识)都是0。同时启动选举定时器,每个节点的选举定时器都不一致且都在100~500ms之间(避免同时发起选举)。

第二阶段:从 Follower 状态转换为Candidate,并发起投票

由于没有 Leader,Followers 无法与 Leader 保持心跳(heart beat),节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转变为 Candidate 状态、Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。

注意:每个节点选举定时器超时时间都在 100 ~ 500 ms之内,且不一致。因此,可以避免所有的Follower同时转化为 Candidate状态,换言之,最先转为 Candidate 并发起投票请求的节点将具有成为 Leader 的先发优势。

第三阶段:投票策略

Follower 节点收到投票请求后会根据以下情况决定是否接受投票请求:

  • 请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给 Candidate 节点;
  • 请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己。

第四阶段:Candidate 转换为 Leader

经过一轮选举后,正常情况,会有一个 Candidate 节点收到超过半数(N/2+1)其它节点的投票,那么它将胜出并升级为 Leader 节点,然后定时发送心跳给其它节点,其它节点会转化为 Follower 节点并与 Leader 保持同步,如此,本轮选举结束。如果一轮选举中,Candidate 节点收到的投票没有超过半数,那么将进行下一轮选举。

3.2 Raft 日志同步

一个 Raft 集群中只有 Leader 节点能够处理客户端的请求(如果客户端的请求发到了 Follower 节点,Follower 将会把请求重定向到 Leader),客户端的每一个请求都包含一条被复制到状态机执行的指令。Leader 把这条指令作为一条新的日志条目(Entry)附加到日志中去,然后并行的将附加条目发送给 Followers,让它们复制这条日志条目。当这条日志条目被 Followers 安全的复制,Leader 会应用这条日志条目到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢,再或者是网络丢包,Leader 会不断的重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 最终都存储了所有的日志条目,确保强一致性

日志复制详细流程如下:

第一阶段:客户端请求提交到 Leader

Leader 收到客户端请求:如存储一个数据:5;Leader 收到请求后,会将它作为日志条目(Entry)写入本地日志中。此时该 Entry 是未提交状态(uncommitted),Leader并不会更新本地数据,因此它是不可读的。

第二阶段:Leader 将 Entry 发送到其它Follower

Leader 与 Followers 之间保持心跳联系,跟心跳 Leader 将追加的 Entry(AppendEntries) 并行的发送到其它 Follower 节点,并让它们复制这条日志条目,这一过程我们称为:复制(Replication)。

  1. 为什么 Leader 向 Follower 发送的 Entry 是 AppendEntries 呢?

因为 Leader 与 Follower 的心跳是周期性的,而一个周期 Leader 可能接收到客户端的多个请求,因此,随 心跳向 Followers 发送的大概率是多个 Entry,即 AppendEntries。在本例中为了简单,只有一条请求,自然 只有一个 Enrety。

2. Leader 向 Followers 发送的不仅仅是追加的 Entry (AppendEntries)

在发送追加日志条目的时候,Leader 会把新日志条目之前的条目索引(前一个日志条目)位置(prevLogIndex)和Leader任期号(term)包含在里边。如果 Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它会拒接这个新的日志条目。因为出现这种情况说明 Follower 和 Leader 是不一致的。

3. 如何解决 Leader 和 Follower 不一致的问题?

在正常情况下,Leader 和 Follower 的日志保持一致,所以追加日志的一致性从来不会失败。然后,Leader 和 Follower 的一系列崩溃情况下会使它们的日志处于不一致的状态。Follower 可能会丢失一些在新的 Leader 中有的日志条目,它也可能拥有一些 Leader 没有的日志条目,或者两者都有发生。丢失或者多出的日志条目可能会持续多个任期。

要使 Follower 的日志与 Leader 恢复一致,Leader 必须找到最后两者达成一致的地方,然后删除从那个节点之后的所有日志,发送自己的日志给 Follower。所有的这些操作都在进行附加日志一致性检查时完成。

Leader 节点针对每个 Follower 节点维护了一个 nextIndex,这表示下一个需要发送给 Follower 的日志条目的索引地址。当一个 Leader 刚获得权力的时候,它初始化所有的 nextIndex 值为自己的最后一条日志的 index + 1。如果一个 Follower 日志和 Leader 不一致,那么在下一次附加日志的时候就会检查失败。在被 Follower 拒绝之后,Leader 就会减小该 Follower 对应的 nextIndex 值并进行重试(即回溯)。

最终 nextIndex 会在某个位置使得 Leader 和 Follower 的日志达成一致。当这种情况发生,附加日志就会成功,这时就会把 Follower 冲突的日志条目全部删除并且附加上 Leader 的日志。一旦附加成功,那么 Follower 的日志就会和 Leader 保持一致,并且在接下来的任期里一致继续保持。

第三阶段:Leader 等待 Followers 回应

Followers 接收到 Leader 发来的复制请求后,有两种可能的回应:

  • 写入本地日志,返回 Success
  • 一致性检查失败,拒绝写入,返回 false。原因和解决办法上面已经详细说明。

注:此时该 Entry 的状态也是未提交(uncommitted)。完成上述步骤后,Followers 会向 Leader 发出回应 - success,当 Leader 收到大多数 Followers 的回应后,会将第一阶段写入的 Entry 标记为提交状态(committed),并把这条日志条目应用到它的状态机中。

第四阶段:Leader回应客户端

完成前三个阶段后,Leader 会回应客户端 - OK,写操作成功。

第五阶段:Leader 通知 Followers Entry 已提交

Leader 回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态。至此,Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。需要注意的是,由于网络、性能、故障等各种原因导致的“反应慢”、“不一致”等问题的节点,也会最终与 Leader 达成一致。

3.3 Raft 安全性保证

前面的章节里描述了 Raft 算法是如何选举 Leader 和 日志复制。然而,到目前为止描述的机制并不能充分保证每一个状态机会按照相同的顺序执行相同的指令。例如:一个 Follower 可能处于不可用的状态,同时 Leader 已经提交了若干的日志条目;然后这个 Follower 恢复(尚未与 Leader 达成一致)而 Leader 故障,如果该 Follower 被选举为 Leader 并且覆盖这些日志条目,就会出现问题:不同的状态机执行不同的指令序列。

鉴于此,在 Leader 选举的时候需要增加一些限制来完善 Raft 算法。这些限制可保证任何的 Leader 对于给定的任期号(Term),都拥有之前任期的所有被提交的日志条目(所谓 Leader 的完整特性)。

3.3.1 选举限制

对于所有基于 Leader 机制一致性算法,Leader 都必须存储所有已经提交的日志条目。为了保障这一点,Raft 使用了一种简单而有效的办法,以保证之前任期号中已提交的日志条目在选举的时候都会出现在新的Leader中。换言之,日志条目的传送是单向的,只从 Leader 传给 Follower, 并且 Leader 从不会覆盖自身本地日志中已经存在的条目。

Raft 使用投票的方式来阻止一个 Candidate 赢得选举,除非这个 Candidate 包含了所有已经提交的日志条目。Candiate 为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目都在这些服务器节点中肯定存在于至少一个节点上。如果 Candidate 的日志至少和大多数的服务器节点一样新,那么它一定持有了所有已经提交的日志条目。投票请求的限制:请求中包含了 Candidate 的日志信息,然后投票人会拒绝那些日志没有自己日志新的投票请求。

Raft 通过比较两份日志中最后一条日志条目的索引值和任期号,确定谁的日志比较新。如果两份日志最后的条目和任期号不同,那么任期号大的日志更加新一些。如果两份日志最后的任期号相同,那么日志比较长的那个就更加新。

3.3.2 提交之前任期内的日志条目

Leader 知道一条当前任期内的日志记录是可以被提交的,只要它被复制到了大多数 Follower 节点上。如果一个Leader 在提交日志条目之前崩溃了,继任的 Leader 会继续尝试复制这条日志记录。然而,一个 Leader 并不能断定之前任期里的日志条目被保存到大多数 Follower 上就一定已经提交了。很明显,从日志复制的过程可以看出。

鉴于上述情况,Raft 算法不会通过计算副本数的方式去提交一个之前任期内的日志条目。只有 Leader 当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式提交,由于日志匹配特性,之前的日志条目也都会被间接提交。在某些情况下, Leader 可以安全的知道一个老的日志条目是否已经被提交(只需判断该条目是否存储到所有的节点上),但是 Raft 为了简化问题使用一种更加保守的方式。

当 Leader 复制之前任期里的日志时,Raft 会为所有的日志保留原始任期号,这在提交规则上产生了额外的复杂性。但是,这种策略更加容易辨别出日志,因为它可以随着时间和日志变化对日志维护着同一个任期号。此外,该策略使得新 Leader 只需要发送较少的日志条目。

四、扩展与总结

4.1 谁在使用 Raft

使用 Raft 协议的产品可多了,如 Kubernetes,Docker Swarm,Cloud Foundry Diego,CockroachDB,TiDB, etcd,Project Calico,Flannel 等等。只要你的服务有一致性高可用需求,都可以考虑使用 Raft 协议

4.2 Raft 小结

Raft算法具备强一致、高可靠、高可用等优点,具体体现在:

强一致性:虽然所有节点的数据并非实时一致,但 Raft 算法保证 Leader 节点的数据最全,同时所有请求都由Leader 处理,所以在客户端角度看是强一致性的。

高可靠性:Raft算法保证了Committed的日志不会被修改,状态机只应用 Committed 的日志,所以当客户端收到请求成功即代表数据不再改变。Committed 日志在大多数节点上冗余存储,少于一半的磁盘故障数据不会丢失。

高可用性:从Raft算法原理可以看出,选举和日志同步都只需要大多数的节点正常互联即可,所以少量节点故障或网络异常不会影响系统的可用性。即使 Leader 故障,在选举超时到期后,集群自发选举新 Leader,无需人工干预,不可用时间极小。但 Leader 故障时存在重复数据问题,需要业务去重或幂等性保证。

高性能:与必须将数据写到所有节点才能返回客户端成功的算法相比,Raft 算法只需要大多数节点成功即可,少量节点处理缓慢不会延缓整体系统运行。


参考文献:

寻找一种易于理解的一致性算法:github.com/maemual/raft

thesecretlivesofdata: thesecretlivesofdata.com

raftscope:raft.github.io/raftscop

raft: raft.github.io/

编辑于 2019-10-14 17:53