Raft 算法(详细版)

Raft 算法(详细版),第1张

在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利·兰伯特(Leslie Lamport)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。

Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、LibPaxos。此外,Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。

由于 Paxos 算法过于复杂、实现困难,极大地制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。

Raft 算法在斯坦福 Diego Ongaro 和 John Ousterhout 于 2013 年发表的《In Search of an Understandable Consensus Algorithm》中提出。相较于 Paxos,Raft 通过逻辑分离使其更容易理解和实现,目前,已经有十多种语言的 Raft 算法实现框架,较为出名的有 etcd、Consul 。

根据官方文档解释,一个 Raft 集群包含若干节点,Raft 把这些节点分为三种状态:Leader、 Follower、Candidate,每种状态负责的任务也是不一样的。正常情况下,集群中的节点只存在 Leader 与 Follower 两种状态。

Leader(领导者) :负责日志的同步管理,处理来自客户端的请求,与Follower保持heartBeat的联系;

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

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

通常,Raft 集群中只有一个 Leader,其它节点都是 Follower。Follower 都是被动的,不会发送任何请求,只是简单地响应来自 Leader 或者 Candidate 的请求。Leader 负责处理所有的客户端请求(如果一个客户端和 Follower 联系,那么 Follower 会把请求重定向给 Leader)。

为简化逻辑和实现,Raft 将一致性问题分解成了三个相对独立的子问题。

选举(Leader Election) :当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来;

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

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

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

第一阶段:所有节点都是 Follower。

上面提到,一个应用 Raft 协议的集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。

第二阶段:Follower 转为 Candidate 并发起投票。

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

注意,由于每个节点的选举定时器超时时间都在 100-500 毫秒之间,且彼此不一样,以避免所有 Follower 同时转为 Candidate 并同时发起投票请求。换言之,最先转为 Candidate 并发起投票请求的节点将具有成为 Leader 的“先发优势”。

第三阶段:投票策略。

节点收到投票请求后会根据以下情况决定是否接受投票请求(每个 follower 刚成为 Candidate 的时候会将票投给自己):

请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给它;

请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己。

第四阶段:Candidate 转为 Leader。

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

注意:有可能一轮选举中,没有 Candidate 收到超过半数节点投票,那么将进行下一轮选举。

在一个 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,并让它们复制这条日志条目,这一过程称为复制(Replicate)。

有几点需要注意:

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

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

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 达成一致。

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

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

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

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

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

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

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

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

raft 的读写都在 leader 节点中进行,它保证了读的都是最新的值,它是符合强一致性的(线性一致性),raft 除了这个还在客户端交互那块也做了一些保证,详情可以参考论文。但是 zookeeper 不同,zookeeper 写在 leader,读可以在 follower 进行,可能会读到了旧值,它不符合强一致性(只考虑写一致性,不考虑读一致性),但是 zookeeper 去 follower 读可以有效提升读取的效率。

对比于 zab、raft,我们发现他们选举、setData 都是需要过半机制才行,所以他们针对网络分区的处理方法都是一样的。

一个集群的节点经过网络分区后,如一共有 A、B、C、D、E 5个节点,如果 A 是 leader,网络分区为 A、B、C 和 D、E,在A、B、C分区还是能正常提供服务的,而在 D、E 分区因为不能得到大多数成员确认(虽然分区了,但是因为配置的原因他们还是能知道所有的成员数量,比如 zk 集群启动前需要配置所有成员地址,raft 也一样),是不能进行选举的,所以保证只会有一个 leader。

如果分区为 A、B 和 C、D、E ,A、B 分区虽然 A 还是 leader,但是却不能提供事务服务(setData),C、D、E 分区能重新选出 leader,还是能正常向外提供服务。

1)我们所说的日志(log)与状态机(state machine)不是一回事,日志指还没有提交到状态机中的数据。
2)新 leader 永远不会通过计算副本数量提交旧日志,他只能复制旧日志都其他 follower 上,对于旧日志的提交,只能是新 leader 接收新的写请求写新日志,顺带着把旧日志提交了。

上篇讲到了「拜占庭将军问题」:多个拜占庭将军要如何在可能有叛徒、信使可能被策反或者暗杀的情况下达成是否要进攻的一致性决定?还不了解的先看看上一篇 《拜占庭将军问题》 。这篇主要是介绍简化版拜占庭将军问题的解决方案:Raft 共识算法。

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

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

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

我们还是用拜占庭将军的例子来帮助理解 Raft。

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

故事先讲到这里,希望不做技术方面的朋友可以大概能理解 Raft 的原理,下面从比较技术的角度讲讲 Raft 的原理。

从拜占庭将军的故事映射到分布式系统上,每个将军相当于一个分布式网络节点,每个节点有 三种状态:Follower,Candidate,Leader ,状态之间是互相转换的,可以参考下图,具体的后面说。

每个节点上都有一个倒计时器 (Election Timeout),时间随机在 150ms 到 300ms 之间。有几种情况会重设 Timeout:

在 Raft 运行过程中,最主要进行两个活动:

假设现在有如图5个节点,5个节点一开始的状态都是 Follower。

在一个节点倒计时结束 (Timeout) 后,这个节点的状态变成 Candidate 开始选举,它给其他几个节点发送选举请求 (RequestVote)

其他四个节点都返回成功,这个节点的状态由 Candidate 变成了 Leader,并在每个一小段时间后,就给所有的 Follower 发送一个 Heartbeat 以保持所有节点的状态,Follower 收到 Leader 的 Heartbeat 后重设 Timeout。

这是最简单的选主情况, 只要有超过一半的节点投支持票了,Candidate 才会被选举为 Leader ,5个节点的情况下,3个节点 (包括 Candidate 本身) 投了支持就行。

一开始已经有一个 Leader,所有节点正常运行。

Leader 出故障挂掉了,其他四个 Follower 将进行重新选主。

4个节点的选主过程和5个节点的类似,在选出一个新的 Leader 后,原来的 Leader 恢复了又重新加入了,这个时候怎么处理?在 Raft 里,第几轮选举是有记录的,重新加入的 Leader 是第一轮选举 (Term 1) 选出来的,而现在的 Leader 则是 Term 2,所有原来的 Leader 会自觉降级为 Follower

假设一开始有4个节点,都还是 Follower。

有两个 Follower 同时 Timeout,都变成了 Candidate 开始选举,分别给一个 Follower 发送了投票请求。

两个 Follower 分别返回了ok,这时两个 Candidate 都只有2票,要3票才能被选成 Leader。

两个 Candidate 会分别给另外一个还没有给自己投票的 Follower 发送投票请求。

但是因为 Follower 在这一轮选举中,都已经投完票了,所以都拒绝了他们的请求。所以在 Term 2 没有 Leader 被选出来。

这时,两个节点的状态是 Candidate,两个是 Follower,但是他们的倒计时器仍然在运行,最先 Timeout 的那个节点会进行发起新一轮 Term 3 的投票。

两个 Follower 在 Term 3 还没投过票,所以返回 OK,这时 Candidate 一共有三票,被选为了 Leader。

如果 Leader Heartbeat 的时间晚于另外一个 Candidate timeout 的时间,另外一个 Candidate 仍然会发送选举请求。

两个 Follower 已经投完票了,拒绝了这个 Candidate 的投票请求。

Leader 进行 Heartbeat, Candidate 收到后状态自动转为 Follower,完成选主。

以上是 Raft 最重要活动之一选主的介绍,以及在不同情况下如何进行选主。

Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。

一开始,Leader 和 两个 Follower 都没有任何数据。

客户端发送请求给 Leader,储存数据 “sally”,Leader 先将数据写在本地日志,这时候数据还是 Uncommitted (还没最终确认,红色表示)

Leader 给两个 Follower 发送 AppendEntries 请求,数据在 Follower 上没有冲突,则将数据暂时写在本地日志,Follower 的数据也还是 Uncommitted。

Follower 将数据写到本地后,返回 OK。Leader 收到后成功返回, 只要收到的成功的返回数量超过半数 (包含Leader) ,Leader 将数据 “sally” 的状态改成 Committed。( 这个时候 Leader 就可以返回给客户端了)

Leader 再次给 Follower 发送 AppendEntries 请求,收到请求后,Follower 将本地日志里 Uncommitted 数据改成 Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的,

在 Network Partition 的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。

一开始有 5 个节点处于同一网络状态下。

Network Partition 将节点分成两边,一边有两个节点,一边三个节点。

两个节点这边已经有 Leader 了,来自客户端的数据 “bob” 通过 Leader 同步到 Follower。

因为只有两个节点,少于3个节点,所以 “bob” 的状态仍是 Uncommitted。所以在这里, 服务器会返回错误给客户端

另外一个 Partition 有三个节点,进行重新选主。客户端数据 “tom” 发到新的 Leader,通过和上节网络状态下相似的过程,同步到另外两个 Follower。

因为这个 Partition 有3个节点,超过半数,所以数据 “tom” 都 Commit 了。

网络状态恢复,5个节点再次处于同一个网络状态下。但是这里出现了数据冲突 “bob" 和 “tom"

三个节点的 Leader 广播 AppendEntries

两个节点 Partition 的 Leader 自动降级为 Follower,因为这个 Partition 的数据 “bob” 没有 Commit,返回给客户端的是错误,客户端知道请求没有成功,所以 Follower 在收到 AppendEntries 请求时,可以把 “bob“ 删除,然后同步 ”tom”,通过这么一个过程,就完成了在 Network Partition 情况下的复制日志,保证了数据的一致性。

Raft 是能够实现分布式系统强一致性的算法,每个系统节点有三种状态 Follower,Candidate,Leader。实现 Raft 算法两个最重要的事是:选主和复制日志

参考链接:
Raft 官网: >

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/10888980.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-12
下一篇 2023-05-12

发表评论

登录后才能评论

评论列表(0条)

保存