MIT课程分布式系统学习07——Fault Tolerance raft2

MIT课程分布式系统学习07——Fault Tolerance raft2

参考资料

1、MIT 6.824 2020 Robert Morris

2、哔哩哔哩视频 2020 MIT 6.824 分布式系统

3、MIT 6.824翻译

4、CAP

仅供学习,如有侵权,请联系我删除
个人博客:https://yijunquan-afk.github.io/

7.1 Log Backup

让我们想象一下如下的场景:有三台服务器以及日志。

slot 10 11 12 13
S1:  3  
S2:  3  3  4
S3:  3  3  5  

3,4,5分别代表任期号,不用在意这些命令具体是什么。我们假设下一个任期是6,尽管你无法从黑板上确认这一点,但是下一个任期号至少是6或者更大。我们同时假设S3在任期6被选为leader。在某个时刻,新leader S3会发送任期6的第一个日志条目(AppendEntries RPC),来传输任期6的第一个Log,这个Log应该在槽位13。

论文中的图2表示:AppendEntries RPC 实际上不仅是⽤来发送从Client发送给leader的命令,也可以⽤来让所有follower对log进行复制。

这里的AppendEntries消息实际上有两条,因为要发给两个Followers。它们包含了客户端发送给leader的请求。我们现在想将这个请求复制到所有的Followers上。这里的AppendEntries RPC还包含了prevLogIndex字段和prevLogTerm字段。所以leader在发送AppendEntries消息时,会附带前一个槽位的信息。在我们的场景中,prevLogIndex是前一个槽位的位置,也就是12;prevLogTerm是S3上前一个槽位的任期号,也就是5。

slot 10 11 12 13
S1:  3  
S2:  3  3  4
S3:  3  3  5  [6]
    			prevLogIndex = 12
    			prevLogTerm = 5

这样的AppendEntries消息发送给了Followers。Followers在它们收到AppendEntries之前(复制⽇志条⽬之前),会先去进⾏检查。Followers要去检查它们⽇志中接收的前⼀个⽇志条⽬和leader所发送的信息是否匹配(S2的 slot12的信息(term4,接收的命令)是否与S3的slot12的(term5,发送的命令)是否匹配)。

所以对于S2 它显然是不匹配的。S2 在槽位12已经有一个条目,但是它来自任期4,而不是任期5。所以S2将拒绝这个AppendEntries,并返回False给leader。S1在槽位12还没有任何Log,所以S1也将拒绝leader的这个AppendEntries。到目前位置,一切都还好。为什么这么说呢?因为我们不想看到的是,S2 把这条新的Log添加在槽位13。因为这样会破坏Raft论文中图2所依赖的归纳特性,并且隐藏S2 实际上在槽位12有一条不同的Log的这一事实。

不想看到的场景如下:
slot 10 11 12 13
S1:  3  
S2:  3  3  4   6
S3:  3  3  5  [6]
    			prevLogIndex = 12
    			prevLogTerm = 5

所以S1和S2都没有接受这条AppendEntries消息,所以,leader看到了两个拒绝。

leader为每个Follower维护了nextIndex。所以它有一个S2的nextIndex,还有一个S1的nextIndex。之前没有说明的是,如果leader之前发送的是有关槽位13的Log,这意味着leader对于其他两个服务器的nextIndex都是13。这种情况发生在leader刚刚当选,因为Raft论文的图2规定了,nextIndex的初始值是从新任leader的最后一条日志开始,而在我们的场景中,对应的就是槽位13.

不想看到的场景如下:
slot 10 11 12 13
S1:  3  
S2:  3  3  4   6
S3:  3  3  5  [6]
    			prevLogIndex = 12	nextIndex[S2] = 13 
    			prevLogTerm = 5		nextIndex[S1] = 13 

为了响应Followers返回的拒绝,leader会减小对应的nextIndex。所以它现在减小了两个Followers的nextIndex

slot 10 11 12 13
S1:  3  
S2:  3  3  4   6
S3:  3  3  5  [6]
    			prevLogIndex = 12	nextIndex[S2] = 12
    			prevLogTerm = 5		nextIndex[S1] = 12

这一次,leader发送的AppendEntries消息中,prevLogIndex等于11,prevLogTerm等于3。同时,这次leader发送的AppendEntries消息包含了prevLogIndex之后的所有条目,也就是S3上槽位12和槽位13的Log。

slot 10 11 12 13
S1:  3  
S2:  3  3  4   6
S3:  3  3  5  [6]
    			prevLogIndex = 12 11	nextIndex[S2] = 12
    			prevLogTerm = 5  3		nextIndex[S1] = 12

对于S2来说,这次收到的AppendEntries消息中,prevLogIndex等于11,prevLogTerm等于3,与自己本地的Log匹配,所以,S2会接受这个消息。Raft论文中的图2规定,如果接受一个AppendEntries消息,那么需要首先删除本地相应的Log(如果有的话),再用AppendEntries中的内容替代本地Log。所以,S2会这么做:它会删除本地槽位12的记录,再添加AppendEntries中的Log条目。这个时候,S2的Log与S3保持了一致。

slot 10 11 12 13
S1:  3  
S2:  3  3  5   6
S3:  3  3  5  [6]
    			prevLogIndex =12 11	    nextIndex[S2] = 12
    			prevLogTerm = 5 3		nextIndex[S1] = 12

但是,S1仍然有问题,因为它的槽位11是空的,所以它不能匹配这次的AppendEntries。它将再次返回False。而leader会将S1对应的nextIndex变为11,并在AppendEntries消息中带上从槽位11开始之后的Log(也就是槽位11,12,13对应的Log)。

这次的请求可以被S1接受,并得到肯定的返回。现在它们都有了一致的Log。

slot 10 11 12 13
S1:  3  3  5   6
S2:  3  3  5   6
S3:  3  3  5  [6]
    			prevLogIndex =12 11		nextIndex[S2] = 12
    			prevLogTerm = 5 3		nextIndex[S1] = 11

leader在收到了Followers对于AppendEntries的肯定的返回之后,它会增加相应的nextIndex到14。

slot 10 11 12 13
S1:  3  3  5   6
S2:  3  3  5   6
S3:  3  3  5  [6]
    			prevLogIndex =12 11		nextIndex[S2] = 14
    			prevLogTerm = 5 3		nextIndex[S1] = 11

在这里leader使用了一种备份机制(backup mechanism)来探测Followers的Log中,第一个leader的Log相同的位置。在获得位置之后,leader会给Follower发送从这个位置开始的,剩余的全部Log。经过这个过程,所有节点的Log都可以和leader保持一致。

重复一个我们之前讨论过的话题。在刚刚的过程中,我们擦除(erase)了一些Log条目,比如我们刚刚删除了S2中的槽位12的Log。这个位置是任期4的Log。现在的问题是,❓ **为什么Raft系统可以安全的删除这条记录?**毕竟我们在删除这条记录时,某个相关的客户端请求也随之被丢弃了。

slot 10 11 12  13
S1:  3  3  5    6
S2:  3  3 4->5  6
S3:  3  3  5   [6]
    			prevLogIndex =12 11		nextIndex[S2] = 14
    			prevLogTerm = 5 3		nextIndex[S1] = 11

我在上堂课说过这个问题,这里的原理是什么呢?是的,这条Log条目并没有存在于过半服务器中,因此无论之前的leader是谁,发送了这条Log,它都没有得到过半服务器的认可。因此旧的leader不可能commit了这条记录,也就不可能将它应用到应用程序的状态中,进而也就不可能回复给客户端说请求成功了。因为它没有存在于过半服务器中,发送这个请求的客户端没有理由认为这个请求被执行了,也不可能得到一个回复。因为这里有一条规则就是,leader只会在commit之后回复给客户端。客户端甚至都没有理由相信这个请求被任意服务器收到了。并且,Raft论文中的图2说明,如果客户端发送请求之后一段时间没有收到回复,它应该重新发送请求。所以我们知道,不论这个被丢弃的请求是什么,我们都没有执行它,没有把它包含在任何状态中,并且客户端之后会重新发送这个请求。

7.2 Election Restriction

在前面的例子中,我们选择S3作为leader。现在有个问题是,哪些节点允许成为leader

如果你读了Raft论文,那么你就知道答案:为了保证系统的正确性,并非任意节点都可以成为leader不是说第一个选举定时器超时了并触发选举的节点,就一定是leader。Raft对于谁可以成为leader,谁不能成为leader是有一些限制的。

为了证明并非任意节点都可以成为leader,我们这里提出一个例子来证伪。在这个反例中,Raft会选择拥有最长Log记录的节点作为leader,这个规则或许适用于其他系统,实际上在一些其他设计的系统中的确使用了这样的规则,但是在Raft中,这条规则不适用。所以,我们这里需要研究的问题是::❓ **为什么不选择拥有最长Log记录的节点作为leader?**如果我们这么做了的话,我们需要更改Raft中的投票规则,让选民只投票给拥有更长Log记录的节点。

很容易可以展示为什么这是一个错误的观点。我们还是假设我们有3个服务器,现在服务器1(S1)有任期5,6,7的Log,服务器2和服务器3(S2和S3)有任期5,8的Log。

S1:  5  6  7
S2:  5  8  
S3:  5  8

为了避免我们在不可能出现的问题上浪费时间,这里的第一个问题是,这个场景可能出现吗?让我们回退一些时间,在这个时间点S1赢得了选举,现在它的任期号是6。它收到了一个客户端请求,在发出AppendEntries之前,它先将请求存放在自己的Log中,然后它就故障了,所以它没能发出任何AppendEntries消息。

之后它很快就故障重启了,因为它是之前的leader,所以会有一场新的选举。这次,它又被选为leader。然后它收到了一个任期7的客户端请求,将这个请求加在本地Log之后,它又故障了。

S1故障之后,我们又有了一次新的选举,这时S1已经关机了,不能再参加选举,这次S2被选为leader。如果S2当选,而S1还在关机状态,S2会使用什么任期号呢?

明显我们的答案是8,但是为什么任期号是8而不是6呢?尽管没有写在黑板上,但是S1在任期6,7能当选,它必然拥有了过半节点的投票,过半服务器至少包含了S2,S3中的一个节点。如果你去看处理RequestVote代码和Raft论文的图2,当某个节点为候选人投票时,节点应该将候选人的任期号记录在持久化存储中。所里在这里S2或者S3或者它们两者都知道任期6和任期7的存在。因此,当S1故障了,它们中至少一个知道当前的任期是8。这里,只有知道了任期8的节点才有可能当选,如果只有一个节点知道,那么这个节点会赢得选举,因为它拥有更高的任期号。如果S2和S3都知道当前任期是7,那么它们两者中的一个会赢得选举。所以,下一个任期必然为8这个事实,**依赖于不同任期的过半服务器之间必然有重合这个特点。同时,也依赖任期号会通过RequestVote RPC更新给其他节点,并持久化存储,这样出现故障才不会丢失数据。**所以下一个任期号将会是8,S2或者S3会赢得选举。不管是哪一个,新的leader会继续将客户端请求转换成AppendEntries发给其他节点。所以我们现在有了这么一个场景。

现在我们回到对于这个场景的最初的问题,假设S1重新上线了,并且我们又有了一次新的选举,这时候可以选择S1作为leader吗?或者说,可以选择拥有最长Log记录的节点作为leader可以吗?明显,答案是不可以的。

如果S1是leader,它会通过AppendEntries机制将自己的Log强加给2个Followers,这个我们刚刚说过了。如果我们让S1作为leader,它会发出AppendEntries消息来覆盖S2和S3在任期8的Log,并在S2和S3中写入S1中的任期6和任期7的Log,这样所有的节点的Log才能与S1保持一致。:❓为什么我们不能认可这样的结果呢?

因为S2和S3可以组成过半服务器,所以任期8的Log已经被commit了,对应的请求很可能已经执行了,应用层也很可能发送一个回复给客户端了。所以我们不能删除任期8的Log。因此,S1也就不能成为leader并将自己的Log强制写入S2和S3。正因为这个原因,我们不能在选举的时候直接选择拥有最长Log记录的节点。当然,最短Log记录的节点也不行。

在Raft论文的5.4.1,Raft有一个稍微复杂的选举限制(Election Restriction)。这个限制要求,在处理别节点发来的RequestVote RPC时,需要做一些检查才能投出赞成票。这里的限制是,节点只能向满足下面条件之一的候选人投出赞成票:

1️⃣ 候选人最后一条Log条目的任期号大于本地最后一条Log条目的任期号;

2️⃣ 候选人最后一条Log条目的任期号等于本地最后一条Log条目的任期号,且候选人的Log记录长度大于等于本地Log记录的长度

回到我们的场景,如果S2收到了S1的RequestVote RPC,因为S1的最后一条Log条目的任期号是7,而S2的最后一条Log条目的任期号是8,两个限制都不满足,所以S2和S3都不会给S1投赞成票。即使S1的选举定时器的超时时间更短,并且先发出了RequestVote请求,除了它自己,没人会给它投票,所以它只能拿到一个选票,不能凑够过半选票。如果S2或者S3成为了候选人,它们中的另一个都会投出赞成票,因为它们最后的任期号一样,并且它们的Log长度大于等于彼此(满足限制2)。所以S2或者S3中的任意一个都会为另一个投票。S1会为它们投票吗?会的,因为S2或者S3最后一个Log条目对应的任期号更大(满足限制1)。

所以在这里Raft更喜欢拥有更高任期号记录的候选人,或者说更喜欢拥有任期号更高的旧leader记录的候选人。限制2说明,如果候选人都拥有任期号最高的旧leader记录,那么Raft更喜欢拥有更多记录的候选人。

7.3 Log Entries Rollback Scheme

在前面介绍的日志恢复机制中,如果Log有冲突,leader每次会回退一条Log条目,这在许多场景下都没有问题。但是在某些现实的场景中,至少在Lab2的测试用例中,每次只回退一条Log条目会花费很长很长的时间。所以,现实的场景中,可能一个Follower关机了很长时间,错过了大量的AppendEntries消息。这时,leader重启了。按照Raft论文中的图2,如果一个leader重启了,它会将所有Follower的nextIndex设置为leader本地Log记录的下一个槽位。所以,如果一个Follower关机并错过了1000条Log条目,leader重启之后,需要每次通过一条RPC来回退一条Log条目来遍历1000条Follower错过的Log记录。这种情况在现实中并非不可能发生。在一些不正常的场景中,假设我们有5个服务器,有1个leader,这个leader和另一个Follower困在一个网络分区。但是这个leader并不知道它已经不再是leader了。它还是会向它唯一的Follower发送AppendEntries,因为这里没有过半服务器,所以没有一条Log会commit。在另一个有多数服务器的网络分区中,系统选出了新的leader并继续运行。旧的leader和它的Follower可能会记录无限多的旧的任期的未commit的Log。当旧的leader和它的Follower重新加入到集群中时,这些Log需要被删除并覆盖。可能在现实中,这不是那么容易发生,但是你会在Lab2的测试用例中发现这个场景。

所以,为了能够更快的恢复日志,Raft论文在论文的5.3结尾处,对一种方法有一些模糊的描述。原文有些晦涩,在这里我会以一种更好的方式尝试解释论文中有关快速恢复的方法。这里的大致思想是,让Follower返回足够的信息给leader,这样leader可以以任期(Term)为单位来回退,而不用每次只回退一条Log条目。所以现在,在恢复Follower的Log时,如果leader和Follower的Log不匹配,leader只需要对每个不同的任期发送一条AppendEntries,而不用对每个不同的Log条目发送一条AppendEntries。这只是一种加速策略,当然,或许你也可以想出许多其他不同的日志恢复加速策略。

可以让Follower在回复leaderAppendEntries消息中,携带3个额外的信息,来加速日志的恢复。这里的回复是指,Follower因为Log信息不匹配,拒绝了leaderAppendEntries之后的回复。这里的三个信息是指:

1️⃣ XTerm:这个是Follower中与leader冲突的Log对应的任期号。在之前有介绍leader会在prevLogTerm中带上本地Log记录中前一条Log的任期号。如果Follower在对应位置的任期号不匹配,它会拒绝leaderAppendEntries消息,并将自己的任期号放在XTerm中。如果Follower在对应位置没有Log,那么这里会返回 -1。

2️⃣ XIndex:这个是Follower中,对应任期号为XTerm的第一条Log条目的槽位号。

3️⃣ XLen:如果Follower在对应位置没有Log,那么XTerm会返回-1,XLen表示空白的Log槽位数。

我将可能出现的场景分成3类,为了简化,这里只画出一个leader(S2)和一个Follower(S1),S2将要发送一条任期号为6的AppendEntries消息给Follower。

相关文章

显卡天梯图2024最新版,显卡是电脑进行图形处理的重要设备,...
初始化电脑时出现问题怎么办,可以使用win系统的安装介质,连...
todesk远程开机怎么设置,两台电脑要在同一局域网内,然后需...
油猴谷歌插件怎么安装,可以通过谷歌应用商店进行安装,需要...
虚拟内存这个名词想必很多人都听说过,我们在使用电脑的时候...
win11本地账户怎么改名?win11很多操作都变了样,用户如果想要...