万字详解Raft共识算法
介绍
Raft是一种分布式一致性的算法,主要用于管理分布式系统中的复制日志。其设计的主要目标是易于理解和实现,同时保证系统的一致性和可用性。Raft会以库(Library)的形式存在于服务中。如果你有一个基于Raft的多副本服务,那么每个服务的副本将会由两部分组成:应用程序代码和Raft库_(如下图Service和Raft Code)_。应用程序代码接收RPC或者其他客户端请求;不同节点的Raft库之间相互合作,来维护多副本之间的操作同步。
Raft 是为提高可理解性而设计的新共识算法,目的是替代复杂难懂的 Paxos。通过将共识过程拆分为领导选举、日志复制和安全性三个部分,并减少系统中的非确定性,Raft 更易于学习和实现。用户研究表明,学生对 Raft 的理解明显优于 Paxos。此外,Raft 在继承现有共识算法优点的基础上,还引入了一些新的设计特性:
-
强领导者:Raft 采用比其他共识算法更强的领导者机制。例如,日志条目只能从领导者流向其他服务器,这简化了对复制日志的管理,使 Raft 更易理解。
-
领导者选举:Raft 使用随机定时器来选举领导者。这只在原本共识算法所需的心跳机制上增加了少量额外机制,却能简单且快速地解决冲突。
复制状态机的理解
Replicated State Machine 是一种模型,在多个服务器上复制相同的状态机,并通过一致的方式执行相同的指令序列,以确保这些服务器最终达到相同的状态,即使其中部分服务器出现了故障。
复制状态机通常通过复制日志(replicated log)来实现,如下图所示。每台服务器保存一个包含一系列命令的日志,这些命令由其状态机按顺序执行。所有日志中包含的命令顺序相同,因此每台状态机处理的是相同的命令序列。由于状态机是确定性的,因此它们计算出的状态和输出序列也完全一致。
由此可见,需要保持分布式系统中复制日志的一致性,我们需要raft共识算法。每个服务器分为raft层和应用层,客户端将命令传递到service层,raft层会接收来自上层service层的命令,并且将其添加到本地日志中。它通过与其他服务器上的共识模块通信,确保每个日志最终都包含相同顺序的相同请求,即使某些服务器发生故障也不受影响。
一旦命令被正确复制,每台服务器的状态机就按日志顺序处理这些命令,并将结果返回给客户端。这样一来,整个服务器集群就表现得像是一个高度可靠的单一状态机。
所以共识算法要保证在实际系统中可用,就需要具备如下特性:
- 在网络延迟、网络分区、数据包丢失、重复和乱序等分布式系统常见问题中都不会返回错误结果。
- 只要任意多数(majority)服务器正常运行并能彼此通信及与客户端通信,系统就能继续提供服务。(通过Leader保证)
- 算法在保证日志一致性时不依赖于时间,例如不依赖于准确的时钟。(心跳机制)
- 在正常情况下,只要集群中多数服务器对一次远程过程调用(RPC)轮次做出响应,命令就可以完成;少数服务器即使较慢,也不会影响系统整体性能。
Raft相较于Paxos的优势
在论文的下一部分,作者将Raft和Paxos进行了对比,总的来说Paxos协议作为共识算法的代表地位及其存在的两个主要问题:首先,Paxos难以理解,原始描述晦涩复杂,单个决策的设计使得协议难以形成直观理解,扩展到多决策的Multi-Paxos更增加了复杂性;其次,Paxos不利于实际系统的实现,缺乏统一的Multi-Paxos标准,实际系统通常偏离其设计,且其对等节点架构不适合连续决策,实际应用中更倾向于领导者协调,导致实现复杂且容易出错,形式化证明的实用价值有限。鉴于这些问题,作者认为Paxos不适合教学和系统构建,因此设计了Raft协议,旨在提供一个更易理解且更适合实际工程的共识算法替代方案。
Raft算法
Raft通过首先选举一个明确的领导者来实现共识,然后由该领导者全权负责管理复制日志。客户端将消息发送到Leader服务器的service层,领导者接受来自service层的日志条目,将其复制到其他服务器,并通知服务器何时可以安全地将日志条目应用到它们的状态机中。采用领导者机制简化了复制日志的管理,例如,领导者可以在不需咨询其他服务器的情况下决定新条目的日志位置,数据也以简单的方式从领导者流向其他服务器**(只能从Leader流出)**。如果领导者失败或与其他服务器断开连接,则会选举新的领导者。
下面贴一张论文中关于每个RPC的描述,非常重要!在做lab时会参考
Raft基础
一个Raft集群包含多个服务器,例如Raft集群中有五个服务器,那么系统可以容忍两个服务器的故障,因为我们至少要保证一半以上的服务器是正常的**(Leader选举机制)**,在任何时刻,每个服务器都处于三种状态之一:领导者(leader)、跟随者(follower)或候选者(candidate)。正常情况下,集群中只会有一个Leader,其他所有服务器都是跟随者。跟随者是被动的:它们不会主动发起请求,只是响应来自领导者和候选者的请求。领导者处理所有客户端请求(如果客户端联系的是跟随者,跟随者会将客户端重定向到领导者)。下图表面了各个节点之间状态转化的关系:
Raft将时间划分为任意长度的任期(term),任期使用连续的整数编号,每个任期以一次选举开始,其中一个或多个候选者尝试成为领导者。如果某个候选者赢得选举,它将在该任期内担任领导者。在某些情况下,选举可能会导致投票分裂。这种情况下,任期结束时没有领导者;不久后**(每个候选者随机时间)**将开始一个新任期(伴随新的选举)。Raft确保在任一期内最多只有一个领导者。
不同的服务器可能在不同时间观察到任期之间的转换,有时服务器甚至可能错过一次选举或整个任期。任期在Raft中充当逻辑时钟,它们使服务器能够检测过时的信息,比如陈旧的领导者。每个服务器都存储一个当前的任期号,这个号码随着时间单调递增。服务器之间在通信时**(Leader与Follower)会交换当前任期号;如果某个服务器的当前任期比另一个服务器的任期小,它会更新自己的任期号为较大的值。如果候选者或领导者发现自己的任期过时,它会立即退回到跟随者状态。如果服务器收到带有过时任期号的请求(比自己存储的任期小)**,它会拒绝该请求。
领导者选举
Raft使用心跳机制来触发领导者选举。当服务器启动时,它们以跟随者身份开始。只要跟随者持续收到来自领导者或候选者的有效RPC,它就保持在跟随者状态。领导者会定期发送心跳(即不携带日志条目的AppendEntries RPC)给所有跟随者,以维护自己的权威。如果跟随者在一段称为选举超时的时间内没有收到任何通信,它就会认为没有可用的领导者,开始发起选举以选择新的领导者。
当服务器开始选举时,Follower会将当前服务器存储的任期++,并且转变为候选者状态,然后,它会投票给自己,并并行向集群中的其他服务器发送RequestVote RPC。候选者会一直保持这个状态,直到发生以下三种情况之一:
- 赢得选举
- 另一台服务器确立为领导者
- 经过一段时间仍未产生赢家
**情况1:**候选者在某一任期中如果获得了来自集群中大多数服务器的投票,就会赢得选举。在一个任期内,每台服务器最多只能投票给一个候选者,按照“先到先得”的原则进行投票。所以每次选举失败时都会获得新的任期,这是需要将投票重置,便于下一轮投票。多数票规则保证了在一个特定的任期内最多只有一个候选者可以赢得选举,一旦候选者赢得选举,它就会成为领导者。随后,它会向所有其他服务器发送心跳消息,以确立其领导地位并防止新的选举发生。
**情况2:**在等待投票的过程中,候选者可能会收到来自另一台声称是领导者的服务器的 AppendEntries RPC。如果该RPC中包含的任期号大于或等于候选者当前的任期,候选者就会承认这个领导者的合法性并返回到跟随者状态。如果RPC中的任期小于候选者的当前任期,那么候选者会拒绝该RPC并继续保持候选者状态。
**情况3:**候选者既没有赢得选举,也没有明确失败:如果许多跟随者在同一时间都变成候选者,投票可能会被分散,导致没有任何候选者获得多数票。当这种情况发生时,每个候选者都会超时并开始新一轮选举:它们会将自己的任期加一,并发起新一轮的 RequestVote RPC。然而,如果没有额外的措施,这种投票分裂可能会无限重复。
Raft使用随机选举超时机制来确保投票分裂较为罕见且能快速解决。为了尽可能避免投票分裂,选举超时值会从一个固定的区间内随机选取(例如150–300毫秒)。这使得服务器之间的选举时间分散,在大多数情况下只有一台服务器会首先超时;它赢得选举并在其他服务器超时之前就发送出心跳。这个机制也用于应对已经发生的投票分裂:每个候选者在选举开始时会重新启动自己的随机选举超时,并在该超时到期后才开始下一轮选举,这样可以降低新一轮选举再次发生投票分裂的可能性。
日志复制
当Raft集群中的Leader确定之后,它就开始处理客户端请求。每个客户端请求都包含一个需要被复制状态机执行的命令。领导者将该命令作为一个新日志条目追加到自己的日志中,然后并行地向其他服务器发送 AppendEntries RPC,以复制该条目。
当日志条目被半数以上的Follower接收之后,Leader就会将该日志条目应用到自己的状态机上,并将执行结果返回给客户端。这样的日志条目被称为“已提交(committed)”。Raft 保证,已提交的日志条目是持久的,最终会被所有可用的状态机执行。这也意味着领导者日志中在此之前的所有条目也一并被提交,包括前任领导者创建的条目。可以保证之前日志的一致性。
如果某些跟随者宕机或运行缓慢,或者网络数据包丢失,领导者将无限次重试 AppendEntries RPC(即使已经向客户端响应),直到所有跟随者最终都存储了所有日志条目。
日志的组织方式如下:
每个日志条目都存储一个状态机命令,该条目被Leader接收时的任期号。日志条目中的任期号可用于检测日志之间的不一致性。
以下节选自这篇知乎文章:为什么Raft系统这么关注Log,Log究竟起了什么作用?
Raft系统之所以对Log关注这么多的一个原因是,Log是Leader用来对操作排序的一种手段。这对于复制状态机(详见4.2)而言至关重要,对于这些复制状态机来说,所有副本不仅要执行相同的操作,还需要用相同的顺序执行这些操作。而Log与其他很多事物,共同构成了Leader对接收到的客户端操作分配顺序的机制。Log类似于一个数组,每个槽位上的数字就代表了Leader选择的排序
Log的另一个用途是,在一个Follower副本收到了操作,但是还没有执行操作时。该副本需要将这个操作存放在某处,直到收到了Leader发送的新的commit号才执行。所以,对于Raft的Follower来说,Log是用来存放临时操作的地方。Follower收到了这些临时的操作,但是还不确定这些操作是否被commit了。
Log的另一个用途是Leader需要在它的Log中记录操作,因为这些操作可能需要重传给Follower。如果一些Follower由于网络原因或者其他原因短时间离线了或者丢了一些消息,Leader需要能够向Follower重传丢失的Log消息。所以,Leader也需要一个地方来存放客户端请求的拷贝。即使对那些已经commit的请求,为了能够向丢失了相应操作的副本重传,也需要存储在Leader的Log中。
由上述的日志机制构成的日志匹配属性:
- 如果两个日志中的条目具有相同的索引和任期号(index 和 term),那么它们存储的命令是相同的。
- 如果两个日志中的条目具有相同的索引和任期号,那么这两个日志在该条目之前的所有条目也完全相同。
**属性1:**一个领导者在某个特定任期内,最多只能在某个特定的日志索引位置创建一个条目,而且日志条目一旦写入,其位置(索引)是不会改变的。因此,如果两个服务器在相同位置、相同任期号下有一个日志条目,它们必然是同一个命令。
属性2:这个属性是通过 AppendEntries RPC 中的一个简单的一致性检查来保障的:当领导者发送 AppendEntries RPC 时,它会附带新条目之前的那个条目的索引和任期号。跟随者会检查自己日志中是否也有同样的索引和任期号的条目。如果没有匹配,说明之前的日志有不一致,它会拒绝这次追加请求。如果匹配成功,才会接受新条目。(类似于数学归纳法)
在正常运行时,Leader和Follower的日志始终保持一致,因此AppendEntries RPC 检查不会失败,但是如果Leader崩溃的话,可能会出现日志不一致,例如:选举出来新Leader之后,它可能拥有一些旧领导者所没有的额外条目;旧Leader还在进行处理,Follower接收不到旧Leader的日志条目导致缺失。这些不一致情况可能会在Leader和Follower的一系列崩溃中不断累积加剧。
在Raft中,Leader通过强制让Follower的日志与自己的日志保持一致来处理这种不一致性,这意味着,Follower日志中与Leader不一致的条目将被Leader日志中的条目覆盖。
论文下面这段话在做之后的lab有很大的帮助:为了使某个Follower的日志与自己的日志保持一致,Leader必须找到两者日志中最后一次一致的日志条目,然后将自己在该点之后的所有日志发送给Follower,并且Follower自己删除该点之后跟随者中所有日志条目,这些操作都是在 AppendEntries RPC 进行一致性检查时完成的。
领导者为每个跟随者维护一个 nextIndex,表示下一条将发送给该跟随者的日志条目的索引。当一个领导者刚刚当选时,它会将所有 nextIndex 初始化为其日志中最后一条日志的下一个索引(乐观初始化)。
如果某个跟随者的日志与领导者的不一致,那么下一次 AppendEntries RPC 的一致性检查将失败。**在收到拒绝响应后,领导者会将该跟随者的 nextIndex 减一,然后重试 AppendEntries RPC。**如此反复,直到 nextIndex 降到某个值,使得领导者和跟随者的日志在该位置匹配。
借助上述机制,当领导者上任时无需执行特殊操作来恢复日志一致性。它只需开始正常运行,日志就会在 AppendEntries 一致性检查失败后自动趋于一致。领导者从不覆盖或删除自己日志中的条目。
安全性
在前面我们讲解了Raft如何选举领导者和复制日志条目。然而,到目前为止所描述的机制还不足以确保每个状态机以完全相同的顺序执行完全相同的命令。我们可以举出一些例子:一个跟随者在领导者提交了多个日志条目的过程中可能处于不可用状态,随后这个跟随者可能被选为领导者,并用新的条目覆盖这些已提交的条目;这样就可能导致不同的状态机执行不同的命令序列。对此,我们需要对领导者的选举进行限制来完善Raft算法,这个限制确保了任何给定任期的领导者都包含了所有在先前任期中已被提交的日志条目。
在任何基于领导者的共识算法中,领导者最终都必须存储所有已提交的日志条目。比如 Viewstamped Replication(视图复制协议),即使一个领导者最初并不包含所有已提交的条目,它也可能被选为领导者。这类算法包含额外的机制,用于识别缺失的日志条目,并在选举过程中或选举后不久将这些条目传输给新领导者。不幸的是,这会带来大量额外的机制和复杂性。
为了避免识别Leader中缺失的日志条目,Raft采用了一种更简单的方法:Leader从来不会缺失任何已提交的日志,它保证在新领导者被选举出来的那一刻起,之前任期中所有已提交的日志条目就已经存在于该领导者的日志中,无需再传输这些条目,这是一种乐观的方法。所以日志条目只会由Leader向Follower传递,Leader从来都不会覆盖其日志中已有的条目。
Raft通过投票机制保证上面这一点,一个候选人可以赢得一个服务器的选票当且仅当候选人的日志比当前服务器的日志新时__(通过任期和日志索引判断)__才可以通过选票。候选人必须联系集群中超过一半的服务器才能当选,这意味着每一个已提交的条目都至少存在于这些服务器中的一台上。如果有一个候选人可以赢得半数服务器的选票,那么这个候选人的日志就是在当前集群中相对新的,有权威的,保证Leader从不缺失已提交的日志。
Leader知道其当前任期中的某个日志条目一旦被存储在多数服务器上,就可以认为该条目已经被提交。如果Leader在提交某个条目之前崩溃,后续的Leader会尝试完成该条目的复制。然而,Leader不可以因为某个旧任期的条目被多数服务器存储,就立即认定该条目已经被提交。即使一个旧的日志条目已被存储在多数服务器上,它仍有可能被未来的领导者覆盖,如下图所示:
因此,Raft规定:不能仅凭多数副本存在,就断定旧任期的日志是安全提交的。只有当前任期内的条目才可以通过“多数副本”方式被明确地提交。一旦一个当前任期的条目通过这种方式被提交,那么由于‘日志匹配性质’,所有之前的条目也会间接地被视为已提交。Raft 在提交规则上引入了这些额外的复杂性,是因为当领导者复制前任期的日志条目时,这些条目保留了原始的任期号。Raft 中的新领导者发送的旧任期日志条目更少——其他算法必须为了重新编号而发送重复的日志条目。
综上,我们可以得出Raft集群的一些性质:
- 如果某个服务器已经将某个日志索引位置的条目应用到了它的状态机上,那么将不会有其他服务器在相同的索引位置应用不同的日志条目。
- 任何在更高任期中应用该日志索引的服务器,也一定会应用相同的日志条目内容。
- 按照日志索引的顺序依次应用日志条目,所有服务器将以相同的顺序应用完全相同的一组日志条目到它们的状态机中。
Follower和Candi崩溃
在之前的讲解中,我们主要关注了Leader崩溃的情况,而相比之下,follower和candidate的崩溃处理要简单得多,并且二者的处理方式是一样的。如果一个Follower或Candidate崩溃了,那么将发送给它的未来的RequestVote 和 AppendEntries RPC都会失败。Raft通过无限重试机制来处理这些失败,如果崩溃的服务器重新启动了,那么之前失败的 RPC 就会成功完成。
如果某个服务器在完成了一次 RPC,但还未发送响应之前就崩溃了,那它在重启之后仍会接收到相同的 RPC 请求。这不会造成问题,因为 Raft 的 RPC 是幂等的—— 即同一个请求重复执行不会产生副作用。
时间
Raft共识算法有一个核心要求:安全性不能依赖于时间的快慢。也就是说,系统不应因为某个事件发生得比预期更快或更慢而产生错误结果。
例如,如果消息交换的耗时比服务器平均崩溃时间还长,那么候选者可能无法在崩溃前坚持足够长时间来赢得选举;没有一个稳定的领导者,Raft 就无法继续前进。在 Raft 中,领导者选举是对时间最敏感的环节。只要系统满足以下时间条件,Raft 就能够选出并维持一个稳定的领导者:
其中broadcastTime 表示一个服务器向集群中所有服务器并行发送 RPC 并收到响应的平均时间,electionTimeout 是选举超时时间;MTBF(Mean Time Between Failures)是单个服务器的平均故障间隔时间。
这个不等式的意义在于:broadcastTime 应比 electionTimeout 小一个数量级(10 倍以上),这样领导者就能可靠地发送心跳消息,防止跟随者发起选举。借助于 Raft 中使用的随机化选举超时机制,这个条件也减少了选票分裂的概率;electionTimeout 应比 MTBF 小几个数量级(例如 100~1000 倍),这样即使领导者崩溃,系统不可用的时间(约为一个选举超时周期)也只占系统整体运行时间的一小部分,从而保证系统能持续推进。
在 Raft 中,RPC 通常要求接收方将信息持久化到稳定存储中,因此 broadcastTime 大致在 0.5ms 到 20ms 之间,具体取决于存储技术。因此,electionTimeout 适合设置在 10ms 到 500ms 之间。而现代服务器的 MTBF 往往是几个月甚至更长,完全满足上述时间条件。
集群成员变更
在前面的讲解中,我们一直假设集群配置(即参与共识算法的服务器集合)是固定的,但在实际应用中,偶尔需要更改配置,例如在服务器故障时进行替换,或调整复制的程度。虽然可以通过将整个集群下线、更新配置文件并重新启动集群来实现这一点,但这种方式在切换期间会使集群不可用。此外,如果涉及任何手动操作,还可能引发人为错误。
为了避免这些问题,Raft使用自动化配置更改,无需手动干预。为了使配置更改机制是安全的,在转换过程中必须确保不会出现同一任期内选出两个领导者的情况。不幸的是,任何让服务器直接从旧配置切换到新配置的做法都是不安全的。由于无法原子性地让所有服务器同时切换,所以在转换过程中集群可能会分裂成两个独立的多数派,如下图:
为了保证安全,配置更改必须采用两阶段的方法。有多种实现这个两阶段的方式。例如,一些系统在第一阶段禁用旧配置,使其无法处理客户端请求;然后在第二阶段启用新配置。而在 Raft 中,集群首先切换到一种过渡配置,我们称之为联合共识;联合共识使得集群在整个配置变更过程中仍能持续处理客户端请求。一旦联合共识被提交,系统再切换到新配置。联合共识结合了旧配置和新配置的服务器:
- 日志条目会被复制到旧配置和新配置中所有服务器上。
- 来自任一配置的服务器都可以担任领导者。
- 在进行选举和提交日志条目时,必须分别获得旧配置和新配置中各自的多数同意。
集群配置通过复制日志中的特殊条目进行存储和传达。当领导者收到将配置从 Cold 更改为 Cnew 的请求时,它会将联合共识配置作为一个日志条目存储,并通过前面描述的机制进行复制。一旦某个服务器将该新配置条目添加到日志中,它就会在之后的所有决策中使用该配置(服务器始终使用其日志中最新的配置,无论该条目是否已被提交)。这意味着领导者会按照 Cold,new 的规则来判断该配置日志条目是否已经被提交。
如果领导者崩溃,则可能会根据 Cold 或 Cold,new 选出一个新的领导者,这取决于哪个候选人获得了 Cold,new 条目。在此期间,Cnew 不能单方面做出决策。一旦 Cold,new 被提交,Cold 和 Cnew 都不能在未经对方同意的情况下做出决策。并且由前文的领导者选举限制知,只有拥有 Cold,new 日志条目的服务器才可能当选为领导者。时,领导者就可以安全地创建一个描述 Cnew 的日志条目,并将其复制到整个集群中。同样,该配置在每台服务器上被看到时就会立即生效。
当新配置 Cnew 根据其自身规则被提交后,旧配置 Cold 就不再重要,不在新配置中的服务器可以被关闭。在整个过程中,Cold和Cnew都没有同时单方面做出决策v的时刻。保证了系统的安全性。
对于配置变更,还有一些问题需要解决。第一个问题是:新加入的服务器一开始可能没有存储任何日志条目。如果在这种状态下直接将它们添加到集群中,可能需要相当长的时间才能使它们追上其他服务器,在这段时间里就可能无法提交新的日志条目,从而影响可用性。
为了避免中断可用性,Raft在配置变更之前引入了一个额外的阶段:新服务器以无投票权成员的身份加入集群(领导者会将日志条目复制给它们,但它们不参与多数投票),一旦这些新服务器追上了集群中的其他服务器,就可以按照之前描述的流程继续进行配置变更。
第二个问题是:集群的Leader可能不属于新配置:在提交 Cnew 之前,可能只有 Cold 中的服务器才能被选为领导者。在这种情况下,一旦领导者提交了 Cnew 日志条目,就会主动退位(转为 follower 状态)。这意味着在提交 Cnew 的过程中,领导者正在管理一个不再包括自己的集群:它会继续复制日志条目,但在计算多数时不再把自己算在内。
第三个问题是:被移除的服务器(即不属于 Cnew 的服务器)可能会干扰集群的正常运行:这些服务器将收不到心跳消息,因此会超时并尝试发起新的选举。它们会发送带有更大任期号的 RequestVote RPC,这将导致当前Leader因为任期变大而退位为 follower。虽然最终会选出一个新Leader,但这些被移除的服务器又会超时并重复这个过程,从而导致系统可用性很差。
为了防止这个问题,服务器在认为当前仍存在领导者的情况下会忽略RequestVote RPC,具体来说:如果服务器在接收到来自当前领导者的消息之后的最小选举超时时间内收到 RequestVote RPC,它将不会更新任期号,也不会投票。这一点会在接下来的lab中体现。
这个机制不会影响正常选举,因为在正常情况下,每个服务器在发起选举前都会等待至少一个最小选举超时。但是,它可以有效防止被移除服务器的干扰:只要领导者能将心跳正常发送给其集群,就不会因为更大任期号的干扰而被迫退位。
日志压缩
Raft 的日志在正常运行过程中会不断增长,以纳入更多客户端请求,但在实际系统中,日志不可能无限增长。随着日志变得越来越长,它会占用更多空间,并且重放日志的时间也会变长。这最终会引发可用性问题,除非有某种机制可以丢弃日志中累积的过时信息。
快照机制是最简单的日志压缩方法,在快照中,整个当前系统状态会被写入稳定存储中的一个快照,然后从日志中丢弃直到该点为止的全部内容。每个服务器会独立地进行快照操作,仅覆盖其日志中已经提交(committed)的条目。大多数工作由状态机完成,即将其当前状态写入快照文件。Raft 还在快照中包含了一小部分元数据:
- last included index:快照所替代的日志中的最后一个条目的索引
- last included term:该条目的任期(term)。
保留这两个元数据是为了支持 AppendEntries RPC 中的一致性检查机制:因为快照之后的第一个日志条目仍然需要一个“前一个日志索引”和“前一个任期”来进行匹配校验。一旦服务器完成快照写入,它就可以安全地删除所有索引小于等于“last included index”的日志条目,以及此前的任何旧快照。
虽然服务器通常是独立的进行快照,但有时Leader必须将快照发送给落后的Follower,这通常发生在领导者已经丢弃了它即将发送给某个追随者的下一个日志条目时(形成快照之后丢弃了)。
幸运的是,在正常运行中这种情况并不常见:如果一个追随者一直跟得上领导者,它就已经拥有这些日志条目。然而,如果某个追随者非常慢,或者是新加入集群的服务器,它们就可能缺少这些条目。此时,使该追随者跟上进度的方式就是——领导者通过网络向它发送快照。
领导者使用一种新的RPC,称为InstallSnapshot,来向那些落后太多的追随者发送快照,如上图。当追随者收到这个快照RPC时,它必须决定如何处理已有的日志条目。通常情况下,快照会包含接收者日志中尚未拥有的新信息。这时,追随者会丢弃其整个日志;因为快照已经覆盖了这些日志,而且日志中可能还包含与快照冲突的未提交条目。
如果追随者收到的快照只是其日志的一个前缀(例如由于重传或误传),那么快照覆盖的日志条目会被删除,但快照之后的日志条目仍然有效,必须保留。
这种快照方式偏离了Raft中“强领导者”的原则,因为追随者可以在不通知领导者的情况下自行生成快照。但我们认为这种偏离是合理的。虽然有领导者有助于避免达成共识时的决策冲突,但在快照阶段,已经达成了共识,因此不存在决策冲突。数据仍然只是从领导者流向追随者,只是追随者现在可以自行重新整理自己的数据。
论文还提出了之前考虑过的一种方法:
我们曾考虑过另一种基于领导者的方法:只有领导者负责创建快照,然后将快照发送给每个追随者。但这种方法有两个缺点。第一,发送快照给每个追随者会浪费网络带宽并且减慢快照过程。每个追随者本身就拥有生成快照所需的信息,通常从本地状态生成快照比通过网络发送接收快照更经济。第二,领导者的实现会更复杂,例如领导者需要在向追随者复制新日志条目的同时并行发送快照,以免阻塞新的客户端请求。
服务器必须决定何时进行快照。如果快照太频繁,会浪费磁盘带宽和能量;如果快照太少,则可能耗尽存储空间,并且在重启时需要花更多时间重放日志。一种简单策略是在日志达到固定字节大小时进行快照。如果这个大小设置得明显大于预期快照大小,快照的磁盘带宽开销就会很小。
客户端交互
Raft 的客户端会将所有请求发送给领导者。当客户端第一次启动时,会连接到一个随机选择的服务器。如果客户端初次连接的服务器不是领导者,该服务器会拒绝客户端的请求,并提供它所知道的最新领导者的信息(AppendEntries 请求中包含领导者的网络地址)。如果领导者崩溃,客户端请求会超时;此时客户端会重新尝试连接其他随机选择的服务器。
Raft的目标是实现线性一致性(每个操作看起来都像是在一台机器上执行那样)。然而,正如前面描述的那样,Raft 可能会多次执行同一条命令:例如,领导者在提交日志条目但尚未响应客户端时崩溃,客户端会向新的领导者重试该命令,从而导致命令被执行两次。因此我们需要保证命令的幂等性:解决方案是让客户端为每条命令分配唯一的序列号。然后,状态机会跟踪每个客户端已处理的最新序列号及其对应的响应。如果收到的命令序列号已经执行过,状态机会立即返回之前的响应,而不重复执行该命令。
只读操作可以不写入日志进行处理。但如果没有额外措施,可能会返回陈旧数据,因为响应请求的领导者可能已经被更新的领导者替代,但它还未意识到这一点。线性一致性的读取不能返回陈旧数据,Raft 需要两个额外的预防措施以保证这一点,同时又不使用日志。
首先,领导者必须拥有最新的已提交条目信息。领导者完整性性质保证领导者拥有所有已提交条目,但在其任期开始时,它可能不知道哪些条目已提交。为了解决这个问题,领导者需要提交其任期内的一个条目。Raft 的做法是让每个领导者在任期开始时向日志中提交一个空的无操作条目。
其次,领导者在处理只读请求之前,必须确认自己没有被罢免(如果有更新的领导者当选,当前领导者的信息可能已经陈旧)。Raft 通过要求领导者在响应只读请求前,与集群中多数节点交换心跳消息来实现这一点。
性能
接下来是关于Raft性能的一些测试,本文在此不进行过多赘述,因为与Raft原理无关。感兴趣的可以去看论文。
总结
Raft 是一种为了解决分布式系统中共识问题而设计的新算法,它的最大特点是可理解性强。与传统的 Paxos 算法相比,Raft 在保证等价的安全性和容错性的同时,通过清晰的模块划分和直观的状态转换机制,使开发者和学生更容易掌握其核心思想。Raft 将共识过程分解为领导选举、日志复制和安全性保证三个子问题,并引入了如日志压缩(快照)、线性一致读写等实用机制,增强了算法在工程实践中的可用性。Raft 的设计理念表明,一个易于理解的算法不仅更易于实现和维护,也更有助于构建健壮的分布式系统。