6.9 可能的异常情况
最后更新于
最后更新于
一个旧Leader在各种奇怪的场景下故障之后,为了恢复系统的一致性,一个新任的Leader如何能整理在不同副本上可能已经不一致的Log?
这个话题只在Leader故障之后才有意义,如果Leader正常运行,Raft不太会出现问题。如果Leader正在运行,并且在其运行时,系统中有过半服务器。Leader只需要告诉Followers,Log该是什么样子。Raft要求Followers必须同意并接收Leader的Log,这在Raft论文的图2中有说明。只要Followers还能处理,它们就会全盘接收Leader在AppendEntries中发送给它们的内容,并加到本地的Log中。之后再收到来自Leader的commit消息,在本地执行请求。这里很难出错。
在Raft中,当Leader故障了才有可能出错。例如,旧的Leader在发送消息的过程中故障了,或者新Leader在刚刚当选之后,还没来得及做任何操作就故障了。所以这里有一件事情我们非常感兴趣,那就是在一系列故障之后,Log会是怎样?
这里有个例子,假设我们有3个服务器(S1,S2,S3),我将写出每个服务器的Log,每一列对齐之后就是Log的一个槽位。我这里写的值是Log条目对应的任期号,而不是Log记录的客户端请求。所以第一列是槽位1,第二列是槽位2。所有节点在任期3的时候记录了一个请求在槽位1,S2和S3在任期3的时候记录了一个请求在槽位2。在槽位2,S1没有任何记录。
所以,这里的问题是:这种情况可能发生吗?如果可能发生,是怎么发生的?
这种情况是可能发生的。假设S3是任期3的Leader,它收到了一个客户端请求,之后发送给其他服务器。其他服务器收到了相应的AppendEntries消息,并添加Log到本地,这是槽位1的情况。之后,S3从客户端收到了第二个请求,它还是需要将这个请求发送给其他服务器。但是这里有三种情况:
发送给S1的消息丢了
S1当时已经关机了
S3在向S2发送完AppendEntries之后,在向S1发送AppendEntries之前故障了
现在,只有S2和S3有槽位2的Log。Leader在发送AppendEntries消息之前,总是会将新的请求加到自己的Log中(所以S3有Log),而现在AppendEntries RPC只送到了S2(所以S2有Log)。这是不同节点之间Log不一样的一种最简单的场景。我们现在知道了它是如何发生的。
如果现任Leader S3故障了,首先我们需要新的选举,之后某个节点会被选为新的Leader。接下来会发生两件事情:
新的Leader需要认识到,槽位2的请求可能已经commit了,从而不能丢弃。
新的Leader需要确保S1在槽位2记录与其他节点完全一样的请求。
这里还有另外一个例子需要考虑。还是3个服务器,这次我会给Log的槽位加上数字,这样更方便我们后面说明。我们这里有槽位10、11、12、13。槽位10和槽位11类似于前一个例子。在槽位12,S2有一个任期4的请求,而S3有一个任期5的请求。在我们分析之前,我们需要明白,发生了什么会导致这个场景?我们需要清楚这个场景是否真的存在,因为有些场景不可能存在我们也就没必要考虑它。所以现在的问题是,这种场景可能发生吗?
这种场景是可能发生的。我们假设S2在槽位12时,是任期4的新Leader,它收到了来自客户端的请求,将这个请求加到了自己的Log中,然后就故障了。
因为Leader故障了,我们需要一次新的选举。我们来看哪个服务器可以被选为新的Leader。这里S3可能被选上,因为它只需要从过半服务器获得认可投票,而在这个场景下,过半服务器就是S1和S3。所以S3可能被选为任期5的新Leader,之后收到了来自客户端的请求,将这个请求加到自己的Log中,然后故障了。之后就到了例子中的场景了。
因为可能发生,Raft必须能够处理这种场景。在我们讨论Raft会如何做之前,我们必须了解,怎样才是一种可接受的结果。大概看一眼这个图,我们知道在槽位10的Log,3个副本都有记录,它可能已经commit了,所以我们不能丢弃它。类似的在槽位11的Log,因为它被过半服务器记录了,它也可能commit了,所以我们也不能丢弃它。在槽位12记录的两个Log(分别是任期4和任期5),都没有被commit,所以Raft可以丢弃它们。这里没有要求必须都丢弃它们,但是至少需要丢弃一个Log,因为最终你还是要保持多个副本之间的Log一致。
学生提问:槽位10和11的请求必然执行成功了吗?
Robert教授:对于槽位11,甚至对于槽位10,我们不能从Log中看出来Leader在故障之前到底执行到了哪一步。有一种可能是Leader在发送完AppendEntries之后就立刻故障了,所以Leader没能收到其他副本的确认,相应的请求也就不会commit,进而也就不会执行这个请求,所以它也就不会发出增加了的commit值,其他副本也就可能也没有执行这个请求。所以完全可能槽位10和槽位11的请求没有被执行。如果Raft能知道这些,那么丢弃槽位10和槽位11的Log也是合法的,因为它们没有被commit。但是从Log上看,没有办法否认这些请求被commit了。换句话说,这些请求可能commit了。所以Raft必须认为它们已经被commit了,因为完全有可能,Leader是在对这些请求走完完整流程之后再故障。所以这里,我们不能排除Leader已经返回响应给客户端的可能性,只要这种可能性存在,我们就不能将槽位10和槽位11的Log丢弃,因为客户端可能已经知道了这个请求被执行了。所以我们必须假设这些请求被commit了。
我们会在下一节课继续这个话题。