> 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-07-raft2/7.1.md).

# 7.1 日志恢复（Log Backup）

（接6.9 的内容）

我们现在处于这样一个场景

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRbv-a5MbeIrL0QDVS%2F-MBRcg248NLB1IOUHkbd%2Fimage.png?alt=media\&token=7529a8dd-f272-4d42-b18a-788f048d2490)

我们假设下一个任期是6。尽管你无法从黑板上确认这一点，但是下一个任期号至少是6或者更大。我们同时假设S3在任期6被选为Leader。在某个时刻，新Leader S3会发送任期6的第一个AppendEntries RPC，来传输任期6的第一个Log，这个Log应该在槽位13。

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

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRbv-a5MbeIrL0QDVS%2F-MBRg4L_2lMmZBXh8Ous%2Fimage.png?alt=media\&token=937c619f-1649-44fe-8db7-b8958ff4d411)

这样的AppendEntries消息发送给了Followers。而Followers，它们在收到AppendEntries消息时，可以知道它们收到了一个带有若干Log条目的消息，并且是从槽位13开始。Followers在写入Log之前，会检查本地的前一个Log条目，是否与Leader发来的有关前一条Log的信息匹配。

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

![我们不想看到的场景](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRbv-a5MbeIrL0QDVS%2F-MBRjlFOFJxOj-kuP_6Z%2Fimage.png?alt=media\&token=ae0e1dc2-023a-4dc4-8454-95981a3cd600)

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

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

为了响应Followers返回的拒绝，Leader会减小对应的nextIndex。所以它现在减小了两个Followers的nextIndex。这一次，Leader发送的AppendEntries消息中，prevLogIndex等于11，prevLogTerm等于3。同时，这次Leader发送的AppendEntries消息包含了prevLogIndex之后的所有条目，也就是S3上槽位12和槽位13的Log。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSjOijBYyB8FT_B2FY%2Fimage.png?alt=media\&token=a5fd21c8-8693-4626-b8d5-2510377e9370)

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

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSliQIKThBT1WjnQr9%2Fimage.png?alt=media\&token=0d4be89d-c556-45f7-a3f6-e0f9ec6050f9)

但是，S1仍然有问题，因为它的槽位11是空的，所以它不能匹配这次的AppendEntries。它将再次返回False。而Leader会将S1对应的nextIndex变为11，并在AppendEntries消息中带上从槽位11开始之后的Log（也就是槽位11，12，13对应的Log）。并且带上相应的prevLogIndex（10）和prevLogTerm（3）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSmMx42kkvAXe5Yuvv%2Fimage.png?alt=media\&token=3ef84f9a-d980-4816-9bbb-91f5a29e90fe)

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

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSmmPN7PxlIL_Ym48l%2Fimage.png?alt=media\&token=89f6616c-dd28-4710-a77f-aae63d5b27aa)

而Leader在收到了Followers对于AppendEntries的肯定的返回之后，它会增加相应的nextIndex到14。&#x20;

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSnN7mSChSGId9ixEY%2Fimage.png?alt=media\&token=8001bf3c-aa42-4a6b-a9c5-ae076cca6ebf)

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

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

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBRljzYEgVezZRH2cVm%2F-MBSotuT39nkaDnfAhqr%2Fimage.png?alt=media\&token=ba62d152-b4ae-4b4c-bf0b-cd5eb56171b2)

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

> 学生提问：前面的过程中，为什么总是删除Followers的Log的结尾部分？
>
> Robert教授：一个备选的答案是，Leader有完整的Log，所以当Leader收到有关AppendEntries的False返回时，它可以发送完整的日志给Follower。如果你刚刚启动系统，甚至在一开始就发生了非常反常的事情，某个Follower可能会从第一条Log 条目开始恢复，然后让Leader发送整个Log记录，因为Leader有这些记录。如果有必要的话，Leader拥有填充每个节点的日志所需的所有信息。
