> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-06-raft1/6.9-ke-neng-de-yi-chang-qing-kuang.md).

# 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没有任何记录。&#x20;

![](/files/-MBQw0qVBbS6oFTd7rZb)

所以，这里的问题是：这种情况可能发生吗？如果可能发生，是怎么发生的？

这种情况是可能发生的。假设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的请求。在我们分析之前，我们需要明白，发生了什么会导致这个场景？我们需要清楚这个场景是否真的存在，因为有些场景不可能存在我们也就没必要考虑它。所以现在的问题是，这种场景可能发生吗？

![](/files/-MBR-3wdLZal8v1Qwl5G)

这种场景是可能发生的。我们假设S2在槽位12时，是任期4的新Leader，它收到了来自客户端的请求，将这个请求加到了自己的Log中，然后就故障了。

![](/files/-MBR-jCKX-LMa8GDdL8u)

因为Leader故障了，我们需要一次新的选举。我们来看哪个服务器可以被选为新的Leader。这里S3可能被选上，因为它只需要从过半服务器获得认可投票，而在这个场景下，过半服务器就是S1和S3。所以S3可能被选为任期5的新Leader，之后收到了来自客户端的请求，将这个请求加到自己的Log中，然后故障了。之后就到了例子中的场景了。

![](/files/-MBR0d1KijXJ9ROLGxIj)

因为可能发生，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了。

我们会在下一节课继续这个话题。
