Raft算法理解

本文深入解析分布式一致性算法Raft,包括其核心概念——Leader选举、日志复制和状态机,以及如何处理故障容错。详细阐述了选举过程中的状态流转(Follower、Candidate、Leader)、Term机制、心跳包与选举超时,以及数据同步的LogReplication。同时,讨论了选举限制以避免数据丢失,日志压缩以节省存储空间,并介绍了实现Raft所需的Server结构和Rpc消息内容。
摘要由CSDN通过智能技术生成

本文是论文《In Search of an Understandable Consensus Algorithm (Extended Version)》的阅读总结和Mit6.824 Lab2的实验总结。
关于本人Lab2的实验目前还没有写成博客,只是完成了代码https://github.com/holdonbush/MIT6.824-Lab2-Raft

Raft是一种分布式一致性算法。一个Raft集群可以包含多个Server,它能够容忍一部分Server的Failure,只需要集群中的一个Majority Server保持正常工作,Raft集群也就能正常工作。

算法基础 - Raft Basics

Raft集群中各个Server内维持了一种状态机的机制,各个Server的状态也会在几个既定的状态之间流转(Follower, Candidate, Leader),这三种角色组成了整个Raft集群。
在这里插入图片描述

  • Follower:Follower是Raft集群的基础,同一时间集群内绝大多数的Server都会是Follower状态。Follower接收来自Leader 的消息(LogEntry),在网络不出现Failure或者自身不会Crash的情况下,保持与Leader 的一致。
  • Candidate:此类角色的Server属于集群中的候选者,代表此类Server具有竞争成为Leader 的权力。Candidate Server有权发起选举,向其他的Server征求选票,在一个集群中的一个Majority Server都投票给一个Candidate时,那个Candidate就是新的Leader。Candidate来源于Follower。
  • Leader:Leader是集群的关键,Follower会同步来自Leader 的数据,这个数据同步的过程只会从Leader到Follower。不存在Follower向Leader同步数据的情况。Leader出了向Follower同步自己的数据(LogEntry)之外,还会不断的向Follower发送心跳包,目的即是让Follower认知到Leader目前仍然存在,而不用再发起选举选出新Leader。但如果发起了新的选举,选出了新的Leader,旧的Leader状态就会再次流转成Follower。

Raft算法中另一个关键的概念就是Term(任期)。
在这里插入图片描述

  • 整个时间轴是按照Term来划分的。一段时间属于一个Term。当然,这个一段时间并不是一个固定的时间,它可以由多种因素决定。甚至也有可能某个Term持续的时间仅仅只是选举的时间,例如图中的t3。
  • Term可以理解成是一个Leader 的任期。因此,一个Term内最多只会存在一个Term。当然,也会存在一个Term没有Leader 的情况,这种情况下,Candidate们会触发下一次的选举,然后Term任期值增加,跳过了之前的那个Term,也就是上面所说的某个Term只持续了一次选举的时间的情况。
  • 如果一个Term内Leader存在,那么这个Term将会一直持续,Leader不断向Follower发送心跳包或者包含数据的数据包。

上述的状态机和Term的概念是Raft算法的基础。在理解这些内容后,接下来开始是Raft算法的细节。

Leader选举 - Leader Election

Raft使用心跳包的机制来触发Leader选举。整个选举的发起,从Follower这个状态开始。要开启一次选举,首先要经历一次Follower的选举Timeout。

  • 在Raft运行过程中,Leader每隔一段时间会向Follower发送心跳包(可能包含数据LogEntry)。Follower只要接收到了这样的心跳包,就会保持自己的Follower状态,直到发生了一次Timeout,即等待了选举超时长度时间,仍然没有收到任何来自Leader 的心跳包。这个时候,就开启了选举的流程。
  • 选举的过程从Follower开始。Follower在经历的一次选举时间超时后,Follower会将自身的Term加1,并且,将自己的状态转变为Candidate;同时,此Candidate发起选举。
  • Candidate在发起选举时,会向其他的Server发送征求选票的Rpc消息(RequestVote)。对于自身的话,则是直接投票给自己。最终,只要此Candidate收到了超过半数的Server的vote选票,那么,它就成为了这个Term的Leader。
  • 对于其他的Server来说,则需要面对多种的收到征求选票Rpc的情况。
    • 如果对于其他的Server来说,收到了来自Candidate的RequestVote Rpc请求,此时,Rpc中的Term字段代表的任期号小于此Server自身的Term任期号,那么,此Server会忽略此Rpc请求,这代表着此Rpc请求其实是一个过期的Rpc请求,选举超时计时器也不需要重置,也不需要将Server状态变成Follower,直接返回false拒绝投票。
    • 如果收到的Rpc请求中的Term字段大于或者等于自身的Term任期号,那么,此Server可以投票给发起此Rpc的Candidate,同时更新自己的Term任期号为最新的任期号。值得注意的是,如果在此Server决定投票后,又收到了来自其他Candidate(可能是由于同一时间多个Follower都经历的选举时间超时,进入了Candidate状态)的RequestVote Rpc,且新Rpc中的Term更大,那可以依旧按照此逻辑(代表有Term更大的Candidate发起选举),但如果新Rpc中的Term跟当前的一致(代表多个Candidate同时发起选举,具有相同的Term),那么,此Server会拒收此Rpc,即,Server对于同一个Term内收到的多个RequestVote Rpc,只会确认收到的第一个Rpc请求。之后的同一Term内的Rpc请求都拒绝投票。

每次其他Server收到来自Candidate的征求选票的请求,都需要重置自身的选举超时计时器,并保持自己Follower的状态。但需要注意的是,同一个Term内,收到了多个Candidate的投票请求,在第一条投票请求时需要重置选举超时计时器并维持Follower状态,之后的投票请求都不再需要重置计时器以及维护状态了,因为在那个Term内这个Follower已经投票给了一位Candidate。

  • 其他Server完成投票后,对于Candidate来说,如果是收到了来自多数Server的投票,那么,可以成为Leader。但如果,没有收到多数的投票,那么,此Term选举失败。Candidate在等待一段超时时间后,进行下一Term 的选举。进行下一次选举,即重复上面的流程(Term也会加1)。
    • 对于一个Term有多个Candidate同时进行选举的情况(多个Follower都同时进入了Candidate状态),那么可能存在这样一种情况:其他Server半数投票给了CandidateA,另一半投票给了CandidateB,也就代表着此时集群中不存在一个Majority的投票结果。A和B两个Candidate都属于是选举失败,两位Candidate在经过了一段选举超时时间后,再进行下一次选举。
    • 上面的这种子情况下,如果说两位Candidate都具有相同的选举超时时间,那也就代表着下一个Term内进行选举的时候,也可能继续出现这样的情况,即半数投票给A半数投票给B,那这样,可能就会一直超时下去。所以,存在这样一种机制:每个Term内开始进行投票选举的时候,每个Candidate的选举超时时间都是不同的。防止出现上面提到的一直超时的情况。
  • Candidate收获选票成功后,Candidate成为Leader,之后,Leader开始向其他Server发送心跳包。其他Server收到心跳包后,状态也就会重新流转会Follower。

以上的流程即为进行选举的基础流程。当然,到此时为止选举上还存在一点点不足的地方,在后续介绍了数据同步即Log Replication的时候再继续介绍。

数据同步 - Log Replication

一旦一位Leader被选举了出来,此Term 的选举就可以终止了。这里的终止并不是从Follower主动发起的一个行为,它其实也是对Leader发送的消息的一种响应。Leader在被选举出来后就会开始向其他Server发送心跳包,其他Server在收到心跳包后,就将自己的状态保持为Follower,直到下一次持续选举超时时间没有收到任何消息。

这里也潜在的表明了一些细节,Leader发送心跳包存在着一个心跳包发送间隔,同时,Follower为竞选Leader也存在一个选举Timeout。所以,为了防止Leader已经选举出来,且网络没有Failure的情况下,一些Follower仍认为没有Leader存在的情况,选举Timeout应该是大于心跳间隔的。

Leader被选举出来后,Leader可以开始服务整个集群,也就要做到同步自身的数据给集群中的其他Server,因为,具有保存来自Client的数据的权力的只有Leader。其他Server如果收到Client的消息,会拒绝来自Client的消息,且可以选择返回Leader 的位置。Leader向其他Server发送的消息(AppendEntries Rpc),在没有数据需要同步的情况下,就是我们说的心跳包;在有数据需要同步的情况下,Rpc中会携带需要同步的内容(LogEntry)。AppendEntries中携带了Leader档期啊的Term,Server收到Rpc后判断Term是否过期,对于过期的Term,Server可以不作处理直接返回。对于没过期的Term,则需要执行数据同步逻辑,同时重置选举超时计时器,并保持自己Follower的状态。下面是数据从Client发送给Leader到Server同步Leader数据的大致流程。

  • Client向集群的Leader发送了一条消息,消息内容举例为某条Command。Leader会将Client 的消息构造成LogEntry。

LogEntry记录即代表着需要同步的数据。它内部主要有两个关键内容:Term任期号和Value内容。Term代表的是此条LogEntry是在哪个Term产生的数据。Value即代表Log内容-Command。

  • 每个Server中都保存了一份[]LogEntry数据,用于存放LogEntry。在这里,集群同步的数据也就是LogEntry,需要保持集群内LogEntry的一致性。

Leader收到Client发送的某条Command,则会根据自己当前的Term和Client发送的Command,构造出LogEntry数据,并保存到自己的[]LogEntry中去。

  • Leader需要不断同步自身的LogEntry列表给其他的Server。

Leader要想同步自身的LogEntry给其他Server,那么,它一定需要掌握对于其他的每个Server,接下来的一条AppendEntries消息,应该从自身的LogEntry的哪条LogEntry开始发送。

具体来说,每个Server都会维护一个nextIndexs列表,代表对于其他的Server,在下一次发送AppendEntries Rpc的时候,Rpc中的LogEntry应该从自身的哪个Index开始同步。当然,这个nextIndexs列表之后在当自己是Leader的时候才会用到。而当Server成为Leader的时候,Server需要初始化这个nextIndex列表,这时,Leader将自己目前LogEntry列表最大index加1作为每个Server 的nextIndex。即假如Leader在被选举出来的时候自身LogEntry长度为10,即Index最大值为9,那么,其他Server的nextIndex都会被初始化为10。之后的同步细节在下面介绍。

这里也就引入了另一个关键概念:LogEntry的Index,在[]LogEntry中的Index。整个集群需要保持一致性,Raft有如下的两条特性:

  1. 如果两条LogEntry分别位于两个Server上的LogEntry列表中,且他们在LogEntry列表中具有相同的Index,那么他们一定存储了相同的value。
  2. 如果两条LogEntry分别位于两个Server上的LogEntry列表中,且他们在LogEntry列表中具有相同的Index,那么位于他们之前的列表中的那些LogEntry也都相同。
  • Leader决定什么时候可以将这些LogEntry给Commit掉,即将他们固定。Commit后,Leader可以开始apply这些Command,应用到系统中。

这里说的应用于系统中,可以理解为执行对应逻辑,比如某条command为将x加1,那么commit后,这条LogEntry相当于就是固定了,即不能被其他Leader同步数据时覆盖,之后Leader apply这些Command时就可以执行这条将x加1的操作。

和之前选举的情况类似,当Leader收到了来自半数以上的Server的确认后,包括Leader自己,即可以将Rpc消息携带的那段LogEntry给Commit掉。一次AppendEntries Rpc中携带的LogEntry不一定只有一条,也即是说,Leader可以选择一次性Commit多条LogEntry。

比如,Leader当前的Term为4,它发送的AppendEntries Rpc中包含了从Term2到Term4之间的所有LogEntry,并且Leader收到了半数以上Server的成功确认,包括自己,那么,Leader就可以把这些LogEntry都Commit。相反,如果Leader没有收到半数以上Server的确认,那么Leader无法去做Commit操作。

Leader会维护一个commitIndex值和lastApplied值。前者代表Leader当前已经commit到了哪条index的LogEntry,后者代表Leader目前已经应用到了哪条index的LogEntry。lastApplied一定是小于等于commitIndex 的。

  • 其他Server收到来自Leader 的AppendEntries Rpc消息。Server确认AppendEntries中的LogEntry可以Append到自己的LogEntry上时,返回Leader成功的消息

这个过程是其他Server与Leader保持同步的过程。AppendEntries Rpc中有LogEntry字段,有Leader目前的commitIndex,以及LogEntry字段中第一条LogEntry在发起Rpc的Leader中的LogEntry列表中的前一条LogEntry的PrevTerm和PrevIndex。
在这里插入图片描述
比如此图,Leader当前的Term是8,最新的Log是Term为6,index为10。加入对某个Server的nextIndex是8,那么AppendEntries中,Term是Leader的8,LogEntry字段是从Index8到Index10的LogEntry,PrevIndex和PreTerm则是Index 7和Term 5。此外,AppendEntries还携带了Leader当前的commitIndex。

Rpc中的PrevTerm和PrevIndex是Server用来确认Rpc携带的LogEntry是否合适的。前面提到在Leader被选举出来的时候,nextIndex都被初始化为Leader当前LogEntry的最大Index加1。因此,nextIndex并不都是一定准确标识着对于某个Server应该从哪条index开始发送LogEntry。所以在AppendEntries中需要PrevTerm和PrevIndex字段来协助判断。

Server检查自己的LogEntry列表中PrevIndex处的LogEntry的Term是否等于PrevTerm。相等则代表Leader发送过来的LogEntry列表可以直接从PrevIndex+1的位置处开始Append,如果此处原本存在数据,则直接覆盖。不等,则代表Leader发送的LogEntry并不正确,此时向Leader返回失败,告知Leader发送的LogEntry并不匹配。

Leader在收到Server 的失败返回后,判断错误是由于发送的LogEntry并不匹配,那么,Leader对此Server的nextIndex减1,然后再次发送AppendEntries。如果仍然不匹配,nextIndex继续减1,直到匹配。

现在假如Server判断完成认为Leader发送的LogEntry与自身的匹配,那么除了将AppendEntries中的LogEntry从PrevIndex+1处开始Append之外,Server自身也需要做commit操作。AppendEntries中携带了Leader的commitIndex字段,Server可以把自身的LogEntry都Commit到commitIndex的位置处。Commit后Server就可以开始apply这些LogEntry。

另外,在这个阶段,其他Server收到来自Leader的心跳包后,都需要重置自己的选举超时计时器。

选举限制 - Election Restriction

在进行Leader选举的过程中,除了上面提到的Leader选举操作基本流程,还存在了一套额外的机制保证Leader 的正确选择。上面已介绍了Server中的LogEntry,选举限制也恰恰是根据LogEntry来进行限制的。

Raft中一个Server要想成为Leader,它必须要拥有最新的LogEntry,才能确保不会出现LogEntry丢失的情况。

比如ServerA现在的LogEntry最新的是Index-10,Term-4;ServerB现在的LogEntry最新的是Index-9,Term-4,且A和B的commitIndex都为最大的Index即10和9。假如现在ServerA和ServerB同时开始了竞争Leader,那么,ServerB就不应该被选举为Leader,因为它的LogEntry没有A的新,如果ServerB最终成为了Leader,那么ServerB后续的LogEntry就会覆盖ServerA在Index-10处的LogEntry,导致Index-10的这条LogEntry丢失。ServerA在index10处的LogEntry已经commit了,但被覆盖了,后面就会引发已经commit的LogEntry不一致的问题。

所以每个Server在进行Leader投票的时候,还需要进行一步LogEntry 是否是“新”的判断。即Candidate的LogEntry要比投票者Follower的LogEntry新。此处关于新的定义有两个要求:

  1. 如果ServerA的最后一条LogEntry的Term大于ServerB的最后一条LogEntry的Term,那么ServerA的LogEntry更新。
  2. 如果ServerA和ServerB的最后一条LogEntry的Term相同,那么最后一条LogEntry的Index更大者更新。

在Server收到RequestVote Rpc时,也会进行这样的判断,给拥有更新LogEntry的Candidate投票。

只Commit当前Term的LogEntry - Commit LogEntry From Current Term

Leader在当前Term内收到超过半数的Server的确认后,可以将同步的LogEntry Commit。但此处存在一种限制,即Leader只能Commit自己Term的LogEntry。即在一次AppendEntries中,携带的LogEntry不包含自己的Term,那么在多数Server确认后,其实并不会Commit。
在这里插入图片描述
以此图为例。S1在最初是Leader,它的Term是2,且复制了LogEntry给S2,紧接着S1Crash了。很明显S1并没有Commit Term为2的LogEntry。然后S5获得了来自S3,S4和S5自己的选票成为Leader,Term为3,接收了来自Client 的一条消息。但S5还没来得及同步这条LogEntry给其他Server就Crash了。所以Term为3的LogEntry也没有被Commit。然后S1重新加入集群,发起选举并轻松成为Leader,此时S1先要完成它的复制工作,即将Term为2 的LogEntry发送给其他Server,所以如图c,Term为2 的LogEntry被复制给了S3。然后S1接收了一条Client的消息,构造了Term为4 的LogEntry。值得注意的是,S1在将Term为2 的LogEntry复制给S3时,就已经完成了Term为2 的LogEntry在集群大多数Server上的复制,这条LogEntry已经具备被Commit的条件。但S1现在不能Commit这条LogEntry。

S1自身的Term是4,对于图d,它没有在Term为2的LogEntry完成复制的时候Commit,现在S1刚构造了Term为4 的消息,就Crash了。然后S5发起选举,重新成为Leader,S5可以拿到赖在S2,S3,S4的选票,然后将自己的Term为3的日志复制给其他所有Server覆盖了原本应该为Term2 的LogEntry。问题关键就在这里,如果S1在完成Term为2 的LogEntry的复制后就Commit了,但S1还没来得及复制自己Term4内的LogEntry就Crash了,那么S5成为Leader后就会覆盖这份已经Commit了的Term为2 的LogEntry。这是不合理的,因为已经Commit的LogEntry相当于是已经固定。

所以S1不会Commit Term为2 的LogEntry。它自身的Term为4,在图e中,它完成了Term为4的LogEntry的复制,这是,它就可以开始Commit。值得注意的是,此时的Commit,会将Term4之前的Term2的LogEntry一并Commit。即Leader在Commit时,会将之前的LogEntry一并Commit。现在Term2和Term4 都Commit 了,即使S1在此时又Crash了,S5也无法赢得选举(存在选举限制),也不会发生Commit了的数据被覆盖的情况。

总的来说,在由于Leader Crash的情况下,后面选举出来的Leader可能会继续进行之前的Term内的LogEntry的复制工作,但Leader只会Commit自己Term内的LogEntry,一旦自己Term内的LogEntry被Commit了,之前Term内的LogEntry也就自然被一并Commit了。

时间 - Time

Raft算法中存在多种时间,整个算法的健壮性也一定程度上依靠时间,大致归纳,存在这些时间:Rpc消息广播耗时,选举超时时间,心跳间隔,MTBF(一个Server出现Failure的平均间隔)等,Raft算法要持续保持Leader 的稳定,需要满足一下的时间关系:

Rpc消息广播耗时 <<(远小于) 选举超时时间 << MTBF

Rpc消息传播耗时要远小于选举超时时间,这样,Leader才能不断的发送心跳包给其他Server,阻止其他Follower成为Candidate。关于心跳包间隔,也需要小于选举超时时间,防止Follower一段时间没有收到心跳包,进出Candidate。

选举超时不能是一个固定的时间,这点我们在上文有过说明,防止出现两位Candidate竞争Leader,各自获得半数的投票,在下一轮选举中有同时开始竞争,且又各自获得半数票,导致集群持续不可用。在每轮选举开始时,各位Candidate的选举超时时间都会在一段范围内变化,比如在200到350ms内变化等,这个变化区间倒不一定是这个区间。这样就可以有效防止每次选举都会有多位Candidate同时征求选票。

Follower Failure可能引发的选举问题

假如Follower与Candidate出现了Failure的情况,Leader发送心跳包给这些Server但没有收到返回,或者根本就发不出去,以及在选举时一些Follower与Candidate出现Failure,这不会对系统造成影响。只需要Leader无限期的继续尝试发送即可,其他Server收到消息后根据Term判断消息是否是有效,根据Rpc中其他字段内容判断是否执行对应逻辑。选举时出现的Failure类似。

  • 选举时可能存在这样一种情况,Follower收到来自Candidate的RequestVote,Follower在决定投票后,RequestVoteReply由于网络Failure丢失。此时,Candidate可以再次发送RequestVote,Follower此时内部已经选择投票给此Candidate,那么,Follower直接返回success即可。这种情况也需要重置超时选举计时器。

列出上面这种情况的原因是我们需要将:同一Term内同一个Candidate对同一Server多次发送RequestVote同一Term内不同Candidate对同一Server发送多条RequestVote这两中情况区分开。第一种是可以成功的case,第二中是失败的case。

日志压缩 - Log Compaction

Server上的LogEntry列表的长度会随着日志的不断Commit而不断增长。但我们不能让LogEntry一直保持无限增长,我们需要压缩LogEntry,节省空间,Raft中,使用Snapshot的方式,在某个时刻,创建当时LogEntry的快照,保存到某个Index 的LogEntry位置的最终状态,生成快照后,就可以丢弃这部分LogEntry。需要注意的是,Server只能为已经Commit了的LogEntry创建Snapshot,创建Snapshot就代表这部分LogEntry已经固定,只有已经Commit的LogEntry满足这样的要求。
在这里插入图片描述
以此图为例,现在Server中的LogEntry的长度为7,现在想要压缩日志,现在日志已经Commit到了Index5处,那么Server就可以压缩Index 1~5的LogEntry,生成一个Snapshot,保存最终状态(图中的x,y的最终结果)和LastIncludedIndex(最后面的LogEntry的Index),LastIncludedTerm(最后面的LogEntry的Term)。然后丢弃这部分的LogEntry,现在LogEntry长度就变成了2,只存在Index为6,7的两条LogEntry。

日志压缩操作并不是只会发生在Leader上。实际上,Raft集群中的每个Server都可以独立的进行日志压缩操作。Follower上执行Snapshot创建,Leader并不会知晓,但这并不会影响集群的正常运行,因为创建Snapshot的LogEntry都已经被Commit。

进行Snapshot创建,需要解决的问题之一就是什么时候执行创建。时间间隔过短就会浪费大量的磁盘带宽和其他资源,时间间隔过长又会导致存储风险。一个简单的策略是当日志长度或者大小达到一个固定值是旧执行Snapshot创建操作。在集群的所有Server上都如此。

现在Snapshot可以被创建,那么就会引出新的问题,就的日志被丢弃,那么如果存在Server还没有来得及复制来自Leader 的旧的日志,或者某些Server经过Failure后重新加入集群后,怎么同步这些已经被丢弃了的日志。接下来介绍InstallSnapshot操作,即对于日志缺失的Server,将Snapshot同步给这些Server。

Leader保存了对于每个Server的nextIndex,代表接下来从哪个Index开始发送LogEntry,以及自己创建的Snapshot的LastIncludedIndex和LastIncludedTerm。那么,在Leader准备发送心跳包的时候,如果发现目标Server的nextIndex小于自己的LastIncludedIndex,也就代表着接下来要发送给这个Server的LogEntry已经被创建的快照然后被丢弃,那么Leader就可以发送InstallSnapshot Rpc给目标Server。InstallSnapshot Rpc中会附带上Leader的快照信息,包括LastIncludedIndex和LastIncludedIndexTerm。

Server在收到了来自Leader的InstallSnapshot Rpc的时候,首先也会判断InstallSnapshot携带的Term,过期的Term Server可以直接不作处理;Term没过期时,可能会出现几种情况:

  • Server自身的LogEntry的最大Index小于InstallSnapshot中的LastIncludedIndex,即Server缺少InstallSnapshot的内容,那么,Server可以丢弃自己的所有LogEntry,被InstallSnapshot的快照所取代,且将自身的状态变成Snapshot中保存的最终状态(这里的状态日志自身的业务内容,比如上面图中的x,y值,直接变成Snapshot中保存的x,y的最新值)。
  • Server自身的LogEntry的最大Index大于InstallSnapshot中的LastIncludedIndex,即Server可能还由于某些原因收到了更多的LogEntry。那么,Server可以丢弃LastIncludedIndex之前的所有LogEntry,用快照的内容替代,LastIncludedIndex之后的LogEntry可以保留,因为可能还有一些用处,它们可能是来自于某些其他Leader的LogEntry。

实现Server和Rpc基本结构 - Implement

上文中已经比较细致的介绍了Raft算法内容,接下来将罗列一些我们目前可以确定的Server需要的结构以及Rpc消息需要携带的内容。

  • Server内部维护内容

Server中首先需要维护Term,代表自身当前的任期;需要有一个voteFor字段,主要用于投票阶段代表自己已经投票给谁;需要维护nextIndex列表,代表对于每个其他Server,自己是Leader时,下一个AppendEntries包从哪条日志开始发送;需要维护commitIndex,代表自身已经commit到了哪条LogEntry;需要维护lastApplied字段,代表自己已经应用到了哪条LogEntry;需要维护logs字段,即LogEntry列表;同时还需要维护自身的status。另外还有选举超时,心跳包间隔(可以作为一个公共变量);LastIncludedIndex和LastIncludedTerm以及Snapshot内容。另外通过一个timer在持续推进状态进行。

type Raft struct {
  // 必要公共字段
  term      int
  voteFor   int // 执行一个Server,voteFor的类型可以不一定是int
  nextIndex []int
  commitIndex int
  lastApplied int
  logs      []LogEntry
  status    Status
  LastIncludeIndex  int
  LastIncludeTerm   int
  snapshotCmd       []byte  // 此处以[]byte为例
  timer       *time.Ticker
  voteTimeout time.Duration
  ...
}

type LogEntry struct {
  Term    int
  Cmd     interface{}
}
  • RequestVote Rpc消息结构
type RequestVoteArgs struct {
  Term     int  //
  Candidate int // 投票发起者
  LastLogIndex int // 用于选举限制判断
  LastLogTerm  int // 用于选举限制判断
  ...
}

type RequestVoteReply struct {
  Term    int
  VoteGranted bool
  ...
}

RequestVoteArgs中携带发起选举时的Term,以及发起选举的Candidate标识。以及携带用于进行选举限制判断的LastLogIndex和LastLogTerm。在Reply的时候Term字段标识的是最新的Term,VoteGranted代表是否同时投票给投票发起者Candidate。

  • AppendEntries Rpc消息结构
type AppendEntriesArgs struct {
  Term    int
  LeaderId  int
  PrevLogIndex  int    //用于Server判断Log是否匹配
  PrevLogTerm   int    //用于Server判断Log是否匹配
  Logs          []LogEntry
  LeaderCommit  int    //Leader当前Commit的位置
  ...
}

type AppendEntriesReply struct {
  Term    int
  Success bool
  ...
}

AppednEntries Rpc是Leader进行消息同步的Rpc消息。但Logs长度为0时,它就成为了心跳包。Args中携带Leader的Term和自身标识Id。携带nextIndex处的前一个位置的LogEntry的Index和Term,即PrevLogIndex和PrevLogTerm。携带需要同步到 Logs,以及Leader当前已经Commit到的Index。Reply中携带最新的Term和是否成功同步。

  • InstallSnapshot Rpc消息结构
type InstallSnapshotArgs struct {
  Term    int
  LeaderId int
  LastIncludeIndex int
  LastIncludeTerm  int
  Data     []byte  // Snapshot结果
  ...
}

type InstallSnapshotReply struct {
  Term int
  ...
}

InstallSnapshotArgs中携带Leader自身Term,自身标识Id,以及快照的最新Index和Term,快照数据即可。返回的Reply中是最新的Term。

这三种Rpc消息,接收者首先都会比较Rpc中的Term和自身的Term,Rpc中的Term如果更小,就代表消息过期,Server可以直接无视这些过期的Rpc消息。

上述的Server和几种Rpc消息的结构都只是根据算法要求,必要的一些字段,我们在自己实现Raft实际场景的时候,可以在这些结构上做一些扩展,添加额外字段,方便实现Raft算法。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值