raft算法浅析

raft算法可分解成六部分:

选举leader:包括检测崩溃和选举新的leader

复制log

leader发生变化时的安全性、可用性和一致性

中立旧leader

客户端交互

修改配置:增加或删除server

raft算法定义了term(任期)的概念。一个term可分为两个阶段:选举和复制日志。选举阶段用于选出一个leader,且仅有一个leader;leader选举出来以后,就可以开始复制日志了。当然,选举阶段可能出现分裂投票的情况,即多个candidate都持有不足半数的投票,没有一个candidate的票数过半,大家互不相让,导致选举超时,这时直接进入下一轮term,重新开始选举。

term默认值为0,然后单调递增。term的主要作用是用于识别出过时信息。比如网络分区时,某一分区的server的term滞后,分区恢复后就能根据term值识别出过期的server,过期的server也可以根据收到的较大的term更新自己的term。

raft算法中,在一个term中,一个server只能是以下三种状态之一:

leader:负责与客户端交互,复制日志

follower:只能接收leader的rpc并作出响应

candidate:选举期间server的状态

状态转换图如下:

server启动时,处于follower状态;由于收不到leader的心跳包,超时转入candidate状态。

candidate状态下,可能会因为收到多数server的投票转入leader状态;或者因为收到了新leader的心跳包而转入follower状态,也可能因为发现了更大的term而转入follower状态;或者是因为上述几种情况均未发生而一直等待至超时,导致进入下一轮term,重新转入candidate状态。

leader状态下,如果发现了更大的term,就会退位,转入follower状态。

上述提到了“发现了更大的term”这种情况,这种情况主要是在系统出现网络分区一段时间后恢复正常时可能出现。

一、选举过程(系统启动时的选举可以和leader崩溃时的选举统一起来)

各follower将term加1,进入新一轮term;然后所有的follower随机等待一段时间(一般取T~2T,T一般取150ms)后转入candidate状态,发起投票(即发送RequestVote RPC),同时开始计时一个election timeout时间;

如果在election timeout超时之前没收到新leader的心跳包或者没收到多数投票,则回到步骤1;

否则,根据情况转入follower状态或者leader状态,开始复制log;如果是follower状态,则监听leader的心跳包,如果心跳包超时,则回到步骤1。

步骤1中要求随机等待一段时间再发起投票是为了防止上文提到的多个candidate同时发起投票导致出现分裂投票的情况。

二、复制log

log结构:log entry由index、term和command组成。

leader收到客户端的命令后,封装成一个log entry,append到本地log中,然后向所有的follower发送AppendEntries RPC。当收到多数follower的响应时,leader认为该log entry已提交,然后可以在本地状态机上执行该命令,并返回结果给客户端,同时通知各个follower该log entry已提交,follower收到该通知后就可以将命令送入状态机执行。

从上述描述可以总结出一点,log entry已提交就是指该log entry在大多数server上都有了备份,且大多数server知晓这一点。

对于那些还没向leader发送响应的follower,leader会不断向它们发送AppendEntries RPC,直到它们成功响应。

log一致性特性:

如果两个server上的log entry有相同的index和term,则

该index中存的命令一定相同;

小于该index的所有log entry 一定相同。

如果某一个log entry已被提交,则该log entry之前的所有log entry(index更小的log entry)均已被提交。

下面解释一下为什么raft算法中的log具有这两个特性。

第一,leader每次复制日志时,会进行AppendEntries RPC调用,该调用包含了这样几个参数:

prevLogIndex:leader的本地log中最新的log entry之前的log entry(因为leader是将客户端命令封装成log entry并append到本地log之后才开始复制日志的,所以才会说“之前”)的index

prevLogTerm:leader的本地log中最新的log entry之前的log entry的term

entries[]:将要复制到follower的log entries,可以不止一条

当leader接收了客户端发起的一个新的命令,并将命令封装成log entry写入了本地log后,他需要将该log entry复制到所有的follower上,这时follower不仅仅是简单地将log entry写入到本地log即可,还需要在写入之前检查所有已有的log entry是否与leader中的一致。follower将prevLogIndex和prevLogTerm这两个参数与自己本地最新的log entry的index和term进行对比,如果相同,可以认为自己的本地log与leader是一致的,然后就可以将新的log entry append到本地log中;如果对比发现不相同,则拒绝append,并返回false。

上述提到的这种检查其实是一种递归检查。这一次检查发现prevLogIndex和prevLogTerm与本地最新的log entry的index和term是匹配的,说明上一次检查时相应的prevLogIndex和prevLogTerm也是匹配的,一直递归到log为空,append第一条log entry时,prevLogIndex和prevLogTerm都为0,也是匹配的。所以每次检查时如果prevLogIndex和prevLogTerm与本地最新的log entry匹配,则之前的所有的log entry也是一致的。

那么follower在检查完之后,如果本地最新的log entry是一致的,则本次将log entry append到本地log中之后,整个log与leader上的log仍然是一致的。

此时已经证明了第一条特性。在说明过程中提到的AppendEntries RPC的参数以及follower的一致性检查是不完整的,只是摘取了与第一条特性相关的点。

第二条特性不知道怎么解释,可以简单理解为log entry是按顺序提交的吗?暂且当结论记住吧( ̄ェ ̄;)

三、leader发生变化

leader发生变化时必须保证安全性,即:

如果当前term的leader判断一条log entry已提交,当leader发生变化时,新的leader的log中必须有该log entry。

raft保证安全性的做法是candidate在发起RequestVote RPC时携带其最后一条log entry的index和term,收到rpc的server要进行如下判断:

如果结果为true,则拒绝给该candidate投票。

等价于:

如果选民的term比candidate的还大,直接拒绝投票;

如果选民的term跟candidate的一样大,就再看log的长度,如果选民的log更长,直接拒绝投票;如果选民的log不比candidate更长,则投票;

如果选民的term比candidate的小,则直接投票。

这种方法能够保证在如下情况下的安全性:

term2的leader s1将log entry(index=4, term=2)复制到s3上后完成该log entry的提交,然后s1崩溃,s4和s5不可能当选为leader。

但是无法保证在如下情况下的安全性:

term4的leader s1将log entry(index=4, term=2)复制到s3上后完成该log entry的提交,然后s1崩溃。根据判决条件,s5是可能当选为leader的。s5一旦当选,将会把自己的log复制到其他server上,这样log entry(index=4, term=2)将被覆盖,即出现了已提交的log entry在leader改变后消失的情况。

针对这种情况,raft对commit规则作出了改进。

leader上的一条老term的log entry需满足如下两个条件才能被认为已提交:

必须在大多数server有备份

至少要有一条当前term的log entry在大多数server上有备份

根据官方视频的示例,貌似对于当前term中创建的log entry,只要在大多数server上备份了就算是已提交了。

在下图的情形中,根据commit的新定义,log entry(index=4, term=2)就不是已提交状态,leader s1也就不会将该命令传入状态机。那么,即使s5当选为新term的leader之后,覆盖掉s1、s2和s3上的log entry(index=4, term=2)也无所谓了。

如果满足了commit的新定义,如下图所示,则log entry(index=4, term=2)就是已提交的,于是也就可以放心地将该log entry传入状态机执行了。s5不可能当选为leader,leader只可能在s1、s2和s3中产生,所以log entry(index=4, term=2)不会被新的leader覆盖掉。

总的来说,新的commit规则和选举规则保证了旧leader上已提交的log entry不会被新的leader覆盖掉。

很自然地,下面就要介绍新leader如何使所有follower上的log与新leader一致。

新leader出现后,与leader上的log相比,follower上的log中未提交的部分可能出现log entry缺失,也可能有多余的log entry。

“缺失”很好理解,就是指follower的log比leader的短。

“多余”包括两种情况:

follower的log比leader长的部分

follower的log中与leader中不同的部分

下图的示例中:

先看第一种情况,(c)、(d)和(f)中index>10的entry都是多余的entry。

再看第二种情况,(e)中index=6,7的entry都是多余的entry;(f)中4<=index<=10的entry都是多余的entry。

leader为每个follower维护一个nextIndex变量。为某个follower维护的nextIndex表示leader要发送给该follower的下一个entry的index。nextIndex初始化为leader的最后一个entry的index加1。

leader根据nextIndex设置好AppendEntries RPC的参数prevLogIndex和prevLogTerm,然后发送给follower。如果follower的响应为false,则将nextIndex减一,然后重复上述操作。

当follower收到leader的AppendEntries RPC后,会进行一致性检查。如果一致性检查通过,则将AppendEntries RPC中的entries复制到自己的log中;否则返回false。

四、中立旧leader

当出现网络分区时,可能会有新的leader出现,当分区恢复时,如何处理两个leader呢?

raft使用term识别旧的leader。

对于任何一个RPC,如果发送方的term小于接收方的term,则接收方拒绝该RPC,发送方收到拒绝后转为follower,并更新自己的term。

如果发送方的term大于接收方的term,则接收方转为follower,并更新自己的term,正常处理RPC请求。

五、客户端协议

如果客户端不知道哪个server是leader,则将命令发送到任何一个server上。接收到该命令的server如果不是server,则返回leader信息,让客户端重定向到leader。

leader收到客户端的命令后,必须等待命令被提交,然后将命令注入状态机执行,最后才将执行结果返回给客户端。

如果客户端的请求超时无响应,则客户端会重发该命令到其他server上,最终重定向到新的leader。

如果leader执行了客户端的命令后,还没来得及将结果返回给客户端就崩溃了,那么客户端会因为超时而重发该命令,这样可能导致同一个命令执行了两次。如何避免这种情况呢?

如果leader接收了客户端的命令后,提交了该entry,还未将命令注入状态机执行就崩溃了,也可能导致该命令被新的leader提交,最终也还是会执行两次。

raft的做法是在客户端发送命令时携带一个唯一的id,leader收到该命令后首先检查本地log中是否已有该id,若有,则直接返回对应命令的结果;否则正常操作。

六、修改配置:增加或删除server

当配置发生变化时,由于网络问题,不能保证新配置在所有的server上同时生效。比如某集群由3台集群扩展成5台机器,新配置可能只在2台新机器和1台旧机器上生效,有2台旧机器仍然在使用旧配置。2台旧机器认为目前集群中只有3台机器,因此它们俩就构成了大多数,也就可以选举出一个leader。同时,3台使用新配置的机器认为目前集群中有5台机器,它们仨就构成了大多数,也可以选举出一个leader。此时集群中就出现了2个leader。

raft规避这种情况的方法是采用联合共识(joint consensus)。在从旧配置过渡到新配置的过程中,增加一个新旧配置同时生效的阶段。具体做法如下图所示:

当leader收到客户端更改配置的命令时,将新旧配置封装到一个log entry(记为Cold+new)中,存入本地log;同时立刻让新配置生效,此时leader中同时存在旧配置和新配置,两者都有效。然后将Cold+new广播给其它server。

follower,也就是其它server,收到Cold+new后,一旦决定将该entry写入本地log,则使新配置立即生效,不用等到leader通知已提交后才生效。

leader如果确认Cold+new已被提交,则集群进入joint consensus阶段。在此阶段,不管是选举还是提交命令,必须得到旧配置有效的机器中的大多数的确认,同时也必须得到新配置有效的机器中的大多数的确认。

当leader确认Cold+new已被提交后,就可以开始将新配置广播到其它server上了。一旦新配置提交成功,则集群就可以使用新配置了。

本文图片截自油管官方视频中的PPT《Raft: A Consensus Algorithm for Replicated Logs》。

欢迎关注博主个人微信公众号~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值