Skip to content

Latest commit

 

History

History
200 lines (196 loc) · 30 KB

README.md

File metadata and controls

200 lines (196 loc) · 30 KB

raft 论文笔记

raft背景

共识算法允许多台机器作为一个集群来工作,保证在某些机器出现状况后还能继续工作。过去的一段时间中,Paxos主导了对共识算法的讨论。但难点在于,尽管有许多尝试使它更容易理解,但Paxos是相当难以理解的。所以,系统建设者们为了让算法明显地体现它的作用,在raft设计的过程中,通过分解(Raft 分解领导者选举、日志复制和安全)和减少状态空间(相对于Paxos,Raft降低了非确定性的程度以及服务器之间不一致的方式)来提高可理解性。
raft的一些feature:
Strong leader:Raft采用了更强的领导形式。如日志条目只从领导者流向其他服务器,这简化了对复制的日志的管理。
Leader election:Raft 使用随机的计时器来选举领导者。
Membership changes:使用了一种新的联合共识的方法改变集群中的服务器集。

复制状态机

服务器集群上的状态机处理相同状态的相同副本,这样即使一些服务器停机,集群也能继续运行。复制状态机被用来解决分布式系统中的各种容错问题。每台服务器存储一个包含一系列命令的日志,其状态机按顺序执行这些命令。每个日志都包含相同顺序的命令,所以每个状态机处理相同的命令序列。保持复制日志的一致性是共识算法的工作。即使一些服务器fail,只要日志保证一致,命令被正确复刻,每个服务器的状态机按照日志顺序处理它们,这些服务器就能形成一个单一的、高度可靠的状态机。
实际系统的共识算法特性:
1.在所有非拜占庭条件下,包括网络延迟、分区、数据包丢失、重复和重新排序,它们都能确保安全(永远不会返回错误的结果)。
2.只要有任何一个服务器在运行,它们就能完全发挥作用(可用)。
3.大多数服务器都在运行,并且可以相互通信和与客户通信。如五台服务器容忍两台服务器故障,它们可能会在以后重新恢复状态。
4.不依赖时间来确保日志的一致性:时钟故障和极端的信息延迟在最坏的情况下会导致可用性问题。
5.正常情况下,只要集群中的大多数服务器对单轮远程调用作出反应,这个命令就可以完成,少数缓慢的服务器不会影响系统的整体性能。

Paxos的缺陷

第一个缺点是,Paxos特别难理解。很少有人能成功地理解它,而且是在付出巨大努力之后。
Paxos的第二个问题是,它没有为建立实际的实现提供一个良好的基础。原因之一是没有广泛认同的多Paxos的算法。Paxos架构对于构建实用的系统来说是一个糟糕的架构;这是单树分解的另一个结果。例如,独立地选择一组日志条目,然后将它们拼接成一个连续的日志,这没有什么好处;这只是增加了复杂性。更简单、更有效的做法是设计一个围绕着日志的系统,新的条目以一个受限的顺序依次添加。

Raft设计

Raft相比Paxos优化的设计思想

1.采用分解的思想:在可能的情况下,把问题分成独立的部分,可以相对独立地解决和理解。例如,在Raft中,我们将领袖选举、日志复制、安全和成员变化分开。
2.通过减少需要考虑的状态数量来简化状态空间,使用随机来简化Raft领袖选举。

raft共识算法

Raft是一种管理复制日志的算法。
1.Raft首先通过选举一个领导者来实现共识,然后让该领导者来完全负责管理日志的复制。领导从客户端接受新的日志条目,将其复制到其他服务器上。拥有一个领导者可以简化对复制日志的管理。例如,领导者可以决定将新条目放置在哪里,而不需要咨询其他服务器,并将数据以一种简单的方式传给其他服务器。
2.领导者可能fail或者与其他服务器断开连接,在这种情况下,会在剩下的服务器选出一个新的领导者。
Raft将共识问题分解为三个相对独立的子问题
1.领导者选举:当现有领导者失败时,必须选择一个新的领导者。
2.日志复制:领导者必须接受来自客户端的日志条目,并在集群中复制它们,迫使其他日志与自己的一致。
3.安全性:
Raft的关键安全属性:
1.选举安全:在一个特定的任期内,最多可以选出一位领导人。
2.追加日志:领导者永远不会覆盖或删除其日志中的条目,它只添加新条目。
3.日志匹配:如果两个日志包含一个具有相同的索引和任期,那么这两份日志的所有条目都是相同的。
4.领导者完整性:如果一个日志条目在一个给定的任期中被提交,那么这个词条必须出现在接下来所有更高任期的领导者的日志中。
5.状态机安全:如果一个服务器在它的状态机上应用了一个给定索引的日志条目,那么其他服务器将永远不会为同一索引应用不同的日志条目。

raft基础

在任何时候,每个服务器都处于领导者、追随者、或候选者三种状态中的一种。在正常操作中,有只有一个领导者,其他所有的服务器都是追随者。追随者是被动的:他们自己不发出任何请求,只是对领导者和候选者的请求做出回应。领导者处理所有客户端的请求(如果客户端联系追随者,追随者将其转给领导者)。候选人,被用来选举一个新的领导者。
Raft将时间划分为任意长度的任期,每个任期以选举开始,在选举中,一个或多个候选人试图成为领导者。如果某位候选人在选举中获胜,那么他将在剩余的任期内担任领导者。在某些情况下选举会导致投票的分裂。在这种情况下,任期将在没有领导者的情况下结束,新的任期(重新选举)将很快开始。Raft会确保在一个特定的任期内最多有一个领导者。
某些情况下,一个服务器可能不会观察到一次选举,甚至是整个任期。任期在Raft中充当逻辑时钟,它们允许服务器检测过时的信息,如过时的领导。服务器都会存储一个当前的任期编号,该编号随着时间的推移单调地随时间增加。每当服务器进行通信时,就会交换当前的任期,更新到比较新的任期去。如果一个候选者或领导者发现它的任期已经过期,它将立即恢复到跟随者状态。如果一个服务器收到一个带有过时的任期的请求,它将拒绝该请求。
Raft服务器使用远程过程调用(RPCs)进行通信,基本的共识算法只需要两种RPCs。RequestVote RPCs由候选人在选举期间发起,AppendEntries RPCs由领导者发起,用于复制日志条目并提供一种心跳形式。此外还会增加了第三个RPC,用于在服务器之间传输快照。如果服务器没有及时收到响应,它们会重试RPC,并以并行方式发出RPC,以获得最佳性能。

leader 选举详情

当服务器启动时,它们开始作为跟随者。服务器只要收到来自领导者或候选人的有效RPC,就一直处于跟随者状态(重置定时器)。领导者会定期发送心跳(AppendEntries RPCs,不携带日志条目)到所有追随者,以重置定时器防止超时,如果一个跟随者在一段时期内没有收到通信,称为选举超时。此时它就认为没有可行的领导者,并开始选举,以选择一个新的领导者。
定时器超时时,追随者增加其当前的期,并转换为候选状态。然后,它为自己投票并向集群中的其他每个服务器发出RequestVote RPCs。候选人继续处于这个状态,直到发生以下三种情况之一:
(a) 它赢得选举
(b) 另一个服务器确立了自己的领导地位
(c) 一段时间内没有赢家
如果一个候选人在同一任期内获得了整个集群中大多数服务器的投票,那么它就赢得了选举。每台服务器在给定的任期内最多为一名候选人投票,以先来后到为原则,多数规则确保最多只有一名候选人能在某届选举中获胜。成为领导者后先向其他服务器发一次心跳来防止新的选举。
在等待投票的过程中,候选人可能会收到一个来自另一个服务器的 AppendEntries RPC,声称自己是领导者。如果领导者的任期(包括在其RPC中)至少是与候选人的当前任期一样大,那么候选人就会就承认该领导者是合法的,并返回到跟随者状态,而如果领导者发来的任期比候选人小,那么候选人发送false,并继续处于候选状态
还有一种情况,如果许多追随者同时成为候选人,选票就会被分割,从而使没有候选人获得多数票,这种情况发生时,每个候选人都将超时,并通过增加其任期和启动新一轮的RequestVote RPC来开始新的选举。然而,如果没有额外的措施,分裂的投票可以无限期地重复。为了解决这种问题,Raft提出使用随机的选举超时来确保分歧票的情况很少,为了从一开始就防止分裂投票,选举超时被从一个固定的时间间隔中随机选择,因此在大多数情况下,只有一个服务器会超时。它赢得了选举,并在其他服务器超时前发送心跳。每个候选人也会在选举开始时重新启动其随机的选举超时。

日志复制详情

一旦一个领导者被选出,它就开始监听客户端请求。每个客户请求都包含一个命令,由复制状态机执行。领导者将该命令作为一个新条目附加到它的日志中,然后向其他每个服务器并行地发出AppendEntries RPCs来复制该条目。当该条目已经被安全复制后,领导者将该条目应用于其状态机,并将执行的结果返回给客户端。如果跟随者崩溃、运行缓慢或者网络数据包丢失,领导者会无限期地重试AppendEntries RPCs(甚至在它已经响应了)直到所有跟随者最终存储了所有的日志条目。每个日志条目都存储了一个状态机命令,以及领导者收到该条目时的任期,这用来检测日志之间的不一致,而日志条目中的整数索引用于识别它在日志中的位置。
领导者决定何时将日志条目应用于状态机是安全的,这样的条目是committed。Raft保证所提交的条目是持久化的并且最终会被所有可用的状态机执行。当领导者创建该条目后,会将其复制到大多数的服务器上,包括由以前的领导者创建而一些服务器没有复制的条目。一旦跟随者得知一个日志条目被提交,它就会按日志顺序将该条目应用到其本地状态机。
Raft的日志机制不仅保证不同服务器上的日志有高度的一致性,而且使其更具可预测性。Raft维护以下属性来维护其日志安全性:
1.如果不同日志中的两个条目具有相同的索引和任期,那么它们存储的是同一命令。
2.如果不同日志中的两个条目具有相同的索引和任期,那么这些日志在所有前面的条目相同。
第一个属性来源于:一个领导者在一个给定的任期中最多创建一个具有给定日志索引的条目,而日志条目不会改变它们在日志中的位置。第二个属性由AppendEntries执行的简单一致性检查来保证。当发送一个AppendEntries RPC时,除了发送新的日志外,还会发送在新条目之前的日志条目的索引和任期,如果跟随者在其日志中没有找到具有相同索引和术语的条目,那么它将拒绝新条目。
在正常运行期间,领导者和追随者的日志保持一致。所以AppendEntries的一致性检查不会失败。然而,领导者的崩溃会使日志不一致(旧的领导者可能没有完全 复制其日志中的所有条目),这些不一致会在一系列领导者和追随者的崩溃中加剧。跟随者的日志可能缺少在领导者身上存在的条目,也可能有多余的条目,而这些条目在领导者身上要么是不存在的,要么两者都有。日志中的缺失和多余的条目可能跨越了多个任期。
在Raft中,领导者通过强迫跟随者重复自己的日志来处理不一致的问题。而为了使跟随者的日志与自己的日志保持一致。领导者必须找到两个日志一致的最新日志条目,删除追随者日志中这条日志之后的任何条目,并向追随者发送领导者在这条日志条目后的所有条目。而具体实现就是领导者为每个跟随者维护nextIndex,它是领导者将发送给跟随者的下一个日志条目的索引。当一个领导者第一次上台时,它将所有的nextIndex值初始化为其日志中最后一个索引之后的索引。如果跟随者的日志与领导者的不一致,则AppendEntries RPC将失败。在拒绝之后,领导者会递减NextIndex并重试AppendEntries RPC。最终,NextIndex将达到 一个领导者和追随者日志匹配的点。当这种情况发生时,AppendEntries就会成功,这时就会删除跟随者日志中任何冲突的条目,并从领导者的日志中添加条目(如果有的话)。一旦AppendEntries成功后,跟随者的日志就会与领导者的一致。并且在余下的时间里保持这种状态。
如果由于拒绝导致出现多次AppendEntries,该协议可以被优化来减少被拒绝的AppendEntries RPC的数量。例如:当拒绝一个AppendEntries请求时,跟随者 返回包括冲突条目的任期和日志中该任期的第一个日志条目的索引。有了这些信息,领导者可以通过匹配追随者在冲突任期内的第一条日志条目是否一致,从而绕过该任期中的所有冲突条目;这样变成每个有冲突条目的任期需要一个 AppendEntries RPC,而不是每个词条一个RPC。

安全性

Raft为了确保每个状态机以相同的顺序执行完全相同的命令,其增加一个对哪些服务器可以被选为领导者的限制,该限制确保了任何给定条款的领导者都包含之前条款中承诺的所有条目。
在任何基于领导者的共识算法中,领导者必须最终存储所有承诺的日志条目。Raft使用投票过程来保证只有日志中包含所有承诺的条目的候选人才能当选。如果 候选人的日志至少与多数人中的日志一样是最新的,即候选人日志能够包括多数人的日志,那么它将持有所有已承诺的条目。RequestVote RPC实现了这个限制:RPC包括有关候选人日志的信息,而如果投票人自己的日志比候选人的日志更新,则拒绝为其投票。Raft通过比较两个日志中最后一个条目的索引和任期来确定哪一个是最新的。
一旦一个条目被存储在大多数服务器上,领导者就知道该条目被提交。而如果一个领导者在提交条目之前崩溃了,未来的领导者将试图进行该条目的复制。然而,领导者不能立刻下定论,这可能会出现错误,Raft从不通过计算副本来提交以前条款的日志条目。只有领导者当前任期内的日志条目才会通过计算副本提交。一旦当前任期的条目以这种方式被提交,那么所有之前的条目都会被间接地提交。

追随者和候选人crash

如果一个追随者或候选人崩溃了,那么未来发给它的RequestVote和AppendEntries RPC将失败,Raft通过无限期地重试来处理这些失败。如果一个服务器在完成RPC后但在响应前崩溃,在它重新启动后会再次收到相同的RPC,raft的RPC是等效的,所以这不会造成任何影响。

raft服务器属性

1.服务器状态:
所有服务器上的持久化状态:
currentTerm:服务器所看到的最新的任期。初始化为0,单调增加。
votedFor:在当前任期内投票选择的候选者ID(如果没有,则为空)。
log[]:每个条目包含状态机的命令,以及条目被领导者收到的任期(第一个索引是1)。
所有服务器上的易失性状态(内存):
commitIndex:已知被提交的最高日志条目的索引(初始化为0,单调地增加)。
lastApplied:最后应用于状态机的最高日志条目的索引(初始化为0,单调增加)。
领导者上的易失性状态(内存):(选举后重新初始化)
nextIndex[]:对于每个服务器,下一个发送给该服务器的日志条目的索引(初始化为领导者最后一条日志索引+1)。
matchIndex[]:对于每个服务器,在服务器上已知的被复制的最高日志条目的索引(初始化为0,单调地增加)。
2.RequestVote RPC(候选人用来收集选票)

Arguments:
term:候选者的任期
candidateId:候选者ID
lastLogIndex:候选者最后一条日志条目的索引
lastLogTerm:候选者的最后一次日志条目的任期

Results:
term:当前任期,以便候选者自我更新
voteGranted:true 表示候选者获得了选票
接受者的操作:
(1).如果服务器的currentTerm大于候选人的term,返回false.
(2).如果服务器的votedFor是null或发来的候选人ID,且候选人的日志至少和接收者的日志一样新,则给予投票.
(3).其他情况,返回false.

3.AppendEntries RPC(由领导者调用以复制日志条目,也用作心跳)
Arguments:
term:领导者的任期。
leaderId:领导者Id,方便追随者重定向客户端请求。
prevLogIndex:紧接在新日志之前的日志条目的索引。
prevLogTerm:prevLogIndex条目的任期。
entries[]:要存储的日志条目(心跳时为空,为了提高效率,可以发送多条)。
leaderCommit:leader的commitIndex。

Results:
term:当前任期,方便leader自我更新。
success:标识位,表示追随者是否成功更新。

接受者的操作:
(1).如果领导者的任期小于追随者当前任期,返回false。
(2).如果日志不包含prevLogIndex的条目,并且其当前任期与prevLogTerm匹配,则回复false。
(3).如果一个现有的条目与一个新的条目冲突(相同的索引,但不同的任期),则删除现有的条目和它后面的所有条目。
(4).现有日志尚未出现的任何新条目要添加。
(5).如果 leaderCommit > commitIndex,则设置 commitIndex = min(leaderCommit, index of last new entry)。

4.服务器的规则
对于所有服务器
(1). 如果 commitIndex > lastApplied: 增加 lastApplied, 应用 log[lastApplied]到状态机。
(2). 如果RPC请求或响应包含term > currentTerm, 则设置 currentTerm = term, 且当前服务器转换为跟随者。
对于追随者:
(1). 对来自候选人和领导者的RPC做出响应。
(2). 如果没有收到来自当前领导者的AppendEntries RPC或授予候选人投票权而选举超时,转换为候选者。
对于候选者:
(1). 在转换为候选者时,开始选举。
(2). 递增currentTerm,并为自己投票,将选举定时器重置。
(3). 向所有其他服务器发送RequestVote RPCs。
(4). 如果收到大多数服务器的投票:成为领导者。 (5). 如果收到来自新领导者的AppendEntries RPC:转换为跟随者。
(6). 如果选举超时:开始新的选举。
对于领导者:
(1). 选举后:在空闲期间重复发送最初的空AppendEntries RPCs(心跳)到每个服务器,以防止选举超时。
(2). 如果收到来自客户端的命令:将条目追加到本地日志,在条目应用于状态机后作出响应。
(3). 如果最后的日志索引≥跟随者的NextIndex:发送AppendEntries RPC,里面包括从nextIndex开始的日志条目。
(4). 如果RPC回复成功:为跟随者更新对应的nextIndex和matchIndex。如果AppendEntries RPC因为日志的不一致而失败,递减nextIndex并重试。
(5). 如果存在一个N,使得N>commitIndex,以及大多数的matchIndex[i] ≥ N,并且 log[N].term == currentTerm:设置commitIndex = N。

集群成员变化

为了使配置变化机制安全,在过渡期间必须没有任何一个点可以让两位领导人当选同一任期,不幸的是,任何让服务器直接从旧配置切换到新配置的方法都是不安全的,不可能在一次原子性操作内把所有的服务器进行切换,所以集群在过渡期间有可能分裂成两个独立的集合。

快照设置

Raft的日志在正常运行期间不断增长,以纳入更多的客户端请求,但在一个实际的系统中,它不能无限制地增长。所以就需要采用快照的压缩方法。在快照中,整个当前系统状态被写入稳定存储的快照中,然后丢弃截至该点的所有日志条目。
每台服务器独立进行快照,快照只包括日志中已提交的条目。Raft还在快照中包含了少量的元数据,如快照所取代的日志条目的最后一条的索引,以及该条目的任期。这些被保留下来是为了支持AppendEntries对快照后的第一个日志条目进行一致性检查。为了实现集群成员的变化,快照还包括日志中的最新配置,即最后包含的索引一旦服务器完成了快照的写入,它可以在这之前所有的日志,以及任何之前的快照。
尽管服务器通常是独立进行快照,但领导者必须偶尔向落后的追随者发送快照。这种情况发生在领导者已经丢弃了它需要发送给跟随者的下一个日志条目。正常操作中,跟上领导者的跟随者已经有了这个条目。然而,一个特别慢的跟随者或一个新加入集群的服务器就没有了。要让这样的跟随者更新得由领导者通过网络向它发送一个快照。
领导者使用一个名为InstallSnapshot的新RPC来发送快照给那些落后的追随者。
InstallSnapshot RPC 内容:
Arguments:
term:领导者的任期
leaderId:领导者Id
lastIncludedIndex:快照会替换所有条目,直至并包括这个索引
lastIncludedTerm:最后包含的索引的任期
offset:字节的偏移量,即块在快照文件中的位置
data[]:快照块的原始字节
done:如果这是最后一个大块,则为true

results:
term:当前任期,方便leader更新自己

接收者操作:
(1).如果领导者的term小于服务器的currentTerm,立即回复。
(2).如果是第一块,则创建新的快照文件(偏移量为0)。
(3).在给定的偏移量处将数据写进快照文件
(4).如果done是false,则回复并等待更多的数据块
(5).保存快照文件,丢弃任何现有的或部分索引较小的快照。
(6).如果现有的日志条目与快照的最后一个条目具有相同的索引和任期,则保留它后面的日志条目并回复,如果没有,删除整个日志。
(7).使用快照内容重置状态机(并加载快照的集群配置)

当跟随者通过这个RPC收到快照时,它必须决定如何处理其现有的日志条目。通常情况下,快照会包含新的信息,而这些不在在接收者的日志中。在这种情况下,跟随者会丢弃它的整个日志;它会全部被快照所取代,并且可能有未提交的条目与快照冲突。而如果随者收到的快照的状态已经保存在日志中,那么被快照覆盖的日志条目将被删除,但快照之后的条目仍然有效,必须保留。
如果考虑另一种快照的实现,即只有领导者会创建一个快照,然后将这个快照发送给它的每个追随者。这会带来两个缺点:首先,向每个追随者发送快照会浪费网络带宽和拖慢快照的进程,对服务器来说,从其本地状态产生快照通常比在网络上发送和接收快照成本要低得多;第二,领导者的实现会更加复杂,领导者将需要在向追随者复制新的日志条目的同时向他们发送快照,这样才不会阻碍新的客户请求。
还有两个影响快照性能的问题:
首先,服务器必须决定何时进行快照,服务器的快照过于频繁,它就会浪费磁盘带宽和计算资源,如果快照的频率太低,它就有可能耗尽其存储容量,并增加重启时加载日志所需的时间。一个简单的策略是当日志达到一个固定的大小时,就进行快照。
第二个性能问题是写一个快照可能需要大量的时间,而我们不希望因此而延误正常的操作。解决的办法是使用写时复制技术,这样就可以接受新的更新而不影响正在写入的快照。如操作系统的写时拷贝支持(例如Linux上的fork) 可以被用来创建整个状态机的内存快照。

raft lab实现

lab概述

本次lab主要是通过raft的共识算法来实现多台服务器共同执行存储任务时,当服务器crash以及重启操作下仍能保证所有服务器的commit操作的顺序与状态一致。该lab实际上并没有在多台机器上进行操作,而是通过在程序中创建多个raft类的实例,每个实例创建各自多个线程来实现领导人选举、心跳、日志复制等,通过raft的设计方法来保证安全性。
首先是根据上述实现配置raft类保存的状态,raft类实例相当于服务器,类里面保存内容: 状态机state_machine
对应的id 服务器身份 投票选择:votedFor, 服务器看到的最新term:current_term 已知被提交的最高日志条目的索引:commit_index 最后应用于状态机的最高日志条目的索引:lastApplied 日志信息:logs 持久化日志类:storage 记录上一次收到RPC的时间:last_received_RPC_time 领导者所要保存的数据:对于每个服务器,下一个发送给该服务器的日志条目的索引nextIndex[];对于每个服务器,在服务器上已知的被复制的最高日志条目的索引matchIndex[]。 并为每个实例开启四个线程,一个给领导者使用负责发送心跳,一个负责选举,一个是领导者负责发送可以commit操作给所有追随者,一个负责判断是否可以进行commit操作来改变状态机状态。 当创建所需要的服务器个数(raft类实例)并初始化后,每个服务器以跟随者的身份开始进行选举,在随机算法下每个服务器有自己的选举超时时间,超过这个时间就会变成候选人,当前任期加1,并为自己投票,然后向其他服务器发送请求投票的rpc,通过rpc的反馈来判断,如果有服务器的任期比候选者大,那么候选者就会变成追随者,否则,统计票数来判断自己能否在当前任期成为领导者,只需要超过半数就可以,因为当前任期如果有其他的候选人,投票的规则会采用先到先得,即其他候选人发现服务器已经选好候选人且不是自己时,返回false表示没争取到选票,这样的策略保证当前任期最多只能产生一个领导者,如果没有选出候选人,那么候选人最后会选举超时并开始新一轮的选举。当产生完一个领导者时,他会迅速更新自身状态如nextIndex和matchIndex[];然后向所有其他服务器发送一次心跳,以便将它们变成跟随者。当服务器变为追随者时,另一个线程就开始发送心跳,实际上我在实现时通过记录上一次上收到rpc通讯的时间,如果领导者经常发生数据给追随者,那么这个时间会一直更新,所以主要这个时间和实际时间不会超过一个固定值,就不需要发送心跳,以此来防止频繁发送心跳占用资源。然后是领导者向各个服务器发起更新日志的rpc,通过匹配机制找到领导者与服务器最大索引的一致的日志,然后让服务器更新日志,当接受到新的客户端的请求时,先写进领导者的日志,但是这一条日志并未确认提交,领导者在让服务器更新日志的时候还会统计当前有多少个服务器的日志已经更新到与领导者一样的状态,如果超过半数,那么领导者在接收到命令之后将会增加自己的commit_index,然后通过线程下一次发送更新日志的rpc来让其他的服务器进行更新,这样就能保证大多数的服务器拥有最新的日志与状态。而raft实例的最后一个线程是用来更新状态机当前状态的,如果发现commit_index比自己最后提交的日志索引大时,就会判断服务器是否有最新的日志,如果有,即可进行提交,这样就能防止如果领导者接受的命令不被承认(领导者出现错误导致该任期的命令不被服务器同意)而影响到其他追随者。除此之外,为了防止状态以及日志的丢失,在更新日志和状态机状态时,会进行持久化存储,这里日志存储是通过序列化放入文件中,当crash或其他原因重启后会先加载持久化数据进行恢复。

存在问题及优化

Raft prevote机制:

在Basic Raft算法中,当一个Follower与其他节点网络隔离,这个Follower在electionTimeout没收到心跳之后,会发起选举,并转为Candidate。每次发起选举时,会把Term加一。由于网络隔离,它既不会被选成Leader,也不会收到Leader的消息,而是会一直不断地发起选举。Term会不断增大。
一段时间之后,这个节点的Term会非常大。在网络恢复之后,这个节点会把它的Term传播到集群的其他节点,导致其他节点更新自己的term,变为Follower。然后触发重新选主,但这个旧的Follower_2节点由于其日志不是最新,并不会成为Leader。整个集群被这个网络隔离过的旧节点扰乱,显然需要避免的。
在PreVote算法中,Candidate首先要确认自己能赢得集群中大多数节点的投票,这样才会把自己的term增加,然后发起真正的投票。其他投票节点同意发起选举的条件是(同时满足下面两个条件):
1.没有收到有效领导的心跳,至少有一次选举超时。
2.Candidate的日志足够新(Term更大,或者Term相同raft index更大)。