raft算法一致性的研究

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

RAFT 算法 - 分布式共识算法,Redis 的 Sentinel 就利用该算法。

  • 服务发现框架:consul、etcd
  • 日志:RocketMQ
  • 数据存储:Tidb、k8s

动态演示
www.kailing.pub/raft/index.html#ele...

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

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

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

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

1.3 Raft 三个子问题

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

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

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

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

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

2. Raft 算法之 Leader Election 原理

根据 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 毫秒之间且并不一致(避免同时发起选举)。

raft算法一致性的研究

所有节点都是 Follower

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

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

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

raft算法一致性的研究

Follower 转为 Candidate 并发起投票

第三阶段:投票策略。

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

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

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

raft算法一致性的研究

投票策略

第四阶段:Candidate 转为 Leader。

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

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

raft算法一致性的研究

Candidate 转为 Leader

3. Raft 算法之 Log Replication 原理

在一个 Raft 集群中,只有 Leader 节点能够处理客户端的请求(如果客户端的请求发到了 Follower,Follower 将会把请求重定向到 Leader),客户端的每一个请求都包含一条被复制状态机执行的指令。Leader 把这条指令作为一条新的日志条目(Entry)附加到日志中去,然后并行得将附加条目发送给 Followers,让它们复制这条日志条目。

当这条日志条目被 Followers 安全复制,Leader 会将这条日志条目应用到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢,再或者网络丢包,Leader 会不断得重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 都最终存储了所有的日志条目,确保强一致性。

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

如下图所示,Leader 收到客户端的请求,比如存储数据 5。Leader 在收到请求后,会将它作为日志条目(Entry)写入本地日志中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader 并不会更新本地数据,因此它是不可读的。

raft算法一致性的研究

客户端请求提交到 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 保持一致,并且在接下来的任期继续保持一致。

raft算法一致性的研究

如何解决 Leader 与 Follower 不一致的问题

第三阶段:Leader 等待 Followers 回应。

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

写入本地日志中,返回 Success;

一致性检查失败,拒绝写入,返回 False,原因和解决办法上面已做了详细说明。

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

raft算法一致性的研究

Leader 等待 Followers 回应

第四阶段:Leader 回应客户端。

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

raft算法一致性的研究

Leader 回应客户端

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

Leader 回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态。至此,Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。

需要注意的是,由于网络、性能、故障等各种原因导致“反应慢”、“不一致”等问题的节点,最终也会与 Leader 达成一致。

raft算法一致性的研究

Leader 通知 Followers Entry 已提交

4. Raft 算法之安全性

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

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

4.1 选举限制

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

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

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

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

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

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

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

选举约束

前面所讲的选举机制,还存在一个问题。例如,当一个跟随者不可用时,领导者提交了几个日志(注:只要有大多数机器存活,raft算法就能正常运转并且保证数据一致性)。然后,这个跟随者从故障中恢复并且被选举为领导者。此时,领导者的日志跟其他跟随者的日志存在冲突。根据前面的日志复制章节,对于这种情况,领导者会强制跟随者复制自己的日志来解决不一致的问题。但是,现在这样做会使得不同服务器的状态机没有以相同的顺序执行相同命令。

所以,对于这种情况,简单的做法就是:不允许这样的跟随者或者候选者称为领导者。
也就是说,新的领导者必须包含所有已提交的日志
也就是说,一个候选者必须包含所有已提交的日志,才能赢得选举
也就是说,RequestVote消息中应该包含候选人的日志信息。如果投票的人发现它的日志比候选人的日志新,它可以拒绝投票

那怎么判断两个日志哪个是最新的呢?通过日志中最后一个日志项的任期和索引来比较。比较过程为:

  • 如果任期不同,那么拥有更大编号任期的日志是最新的。
  • 如果任期相同,那么拥有更大索引的日志是最新的。

基于这个问题,我们对选举过程提出了一个约束:

领导者的完整性:
如果一个日志项在某个任期已经被提交了,那么这个条目一定会出现在更大编号任期内的领导者的日志中。

提交先前任期的日志项

raft中,新的领导者不会提交先前任期的日志(来自先前领导者)。一旦一个来自当前任期的日志项被提交后,(在这个日志项)之前的所有日志项都会被间接提交,因为日志复制章节的日志匹配特性。

相比其他一致性算法,raft算法保持了先前任期日志项的任期,

  • 由于日志项在不同时间、不同日志之间保持相同任期,使得我们更容易推理日志项,使得系统行为可预测。
  • 不需要发送多余的日志项来重新编号先前任期的日志项,减少了网络通信。

安全性证明

  1. 证明领导者完整性。
  2. 由领导者完整性证明状态机安全性。
  3. 状态机安全性 + 按日志索引应用日志 –> 所有状态机以相同的顺序执行相同的命令。

领导者完整性特性

领导者完整性特性:如果某个任期内的某个日志项已提交,那么后面任期的领导者的日志中都会包含这个日志项。

假设 leaderT 在它的任期T内提交了一个日志项,但是这个日志项没有被一些未来任期的领导者存储。假设大于任期 T 的所有任期中,存在一个最小任期 U,它的领导者 leaderU 没有存储这个日志项。

  1. 在选举 leaderU 的时候,它的日志中没有这个已提交的日志项。因为领导者不能删除覆盖或删除自己的日志项。
  2. leaderT 把这个日志项复制给了集群中的大多数服务器,而 leaderU 接收到了集群中大多数服务器投的赞同票。因此,至少有一个服务器即接受了 leaderT 的日志项,也为 leaderU 投了票。这个投票者是达到矛盾的关键点。
  3. 这个投票者一定是先接受了 leaderT 的日志项,再为 leaderU 投了票。如果不是这样的话,这个投票者会拒绝存储 leaderT 的日志项,因为它的当前任期比 leaderT 的当前任期大。相比步骤2,步骤3强调了投票者这两个操作的顺序性
  4. 这个投票者为 leaderU 投票时,它的日志中仍然存储了 leaderT 提交的日志项。因为每个在 [T,U) 的领导者不会移除日志项,跟随者只会移除它们跟领导者冲突的日志。相比步骤3,步骤4强调了投票者投票时的日志内容
  5. 投票者把它的票投给了 leaderU,说明 leaderT 的日志至少是跟投票者是一样新的,也就是说,投票者的日志内容要么跟 leaderT 的日志内容完全一样,要么是 leaderT 日志内容的子集。这个导致了两个矛盾点中的一个。
  6. 首先,如果投票者和 leaderU 的最后一个日志项的任期相同(加上步骤5这个大前提),那么 leaderU 的日志至少跟投票者的日志一样长,所以 leaderU 的日志包含了投票者日志中的每一项。这是个矛盾,因为我们已经假设了leaderU 没有存储这个日志项。
  7. 否则,leaderU 的最后一个日志项的任期大于投票者的。这个日志项的任期是大于 T 的,因为投票者的最后一个日志项的任期至少是 T(从步骤2知道,它包含了任期T内已提交的日志项)。创建 leaderU 最后一个日志项的领导者一定已经包含了任期 T 内已提交的日志项(从假设得知)。然后,根据日志匹配特性,leaderU 的日志一定也包含了这个已提交的日志项,跟假设冲突了。
  8. 因此,所有任期大于 T 的领导者一定包含了任期 T 提交的所有日志项。
  9. 日志匹配特性保证了未来的领导者也将包含那些间接提交的日志项,即大多数服务器存储了这个日志条目,还没有应用到状态机。那么,新的领导者一定是从这些服务器产生,并且他们提交自己任期的日志条目时,也会根据日志匹配特性,一并提交先前任期的日志条目。

状态机安全性

状态机安全性:如果一个服务器已经把某个给定索引的日志项应用到状态机了,那么没有其它服务器会为相同的索引应用不同的日志项。

如果一个服务器应用了某个给定索引的日志项,那么这个日志项一定是已经被提交了的,并且在这个日志项之前,包括这个日志项,服务器的日志和领导者的日志是保持一致的。

根据日志完整性特性,后面任期的领导者也保存了相同的日志项,所以,在后面任期应用这个索引的服务器也会应用同样的值。

5、线性化语义

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

6、后记

对比于 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,还是能正常向外提供服务。

本作品采用《CC 协议》,转载必须注明作者和本文链接
cfun
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!