# 6.6 应用层接口

这一部分简单介绍一下应用层和Raft层之间的接口。你或许已经通过实验了解了一些，但是我们这里大概来看一下。假设我们的应用程序是一个key-value数据库，下面一层是Raft层。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMHxuAZbAON_TgPEbF%2Fimage.png?alt=media\&token=0589b995-2fe4-47ff-85c7-00c43ec0f591)

在Raft集群中，每一个副本上，这两层之间主要有两个接口。

第一个接口是key-value层用来转发客户端请求的接口。如果客户端发送一个请求给key-value层，key-value层会将这个请求转发给Raft层，并说：请将这个请求存放在Log中的某处。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMIXJSnsh3Nl8-9taY%2Fimage.png?alt=media\&token=dc878892-ebba-4e7a-9e0a-2ce267e15036)

这个接口实际上是个函数调用，称之为Start函数。这个函数只接收一个参数，就是客户端请求。key-value层说：我接到了这个请求，请把它存在Log中，并在committed之后告诉我。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMJ7i7J_xc7QkFNHqq%2Fimage.png?alt=media\&token=09eb6054-a559-4bf3-b23b-0d97fa790550)

另一个接口是，随着时间的推移，Raft层会通知key-value层：哈，你刚刚在Start函数中传给我的请求已经commit了。Raft层通知的，不一定是最近一次Start函数传入的请求。例如在任何请求commit之前，可能会再有超过100个请求通过Start函数传给Raft层。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMK4Nr7k5bE9yKoQE0%2Fimage.png?alt=media\&token=1a7ecc88-e0f9-472c-a005-630bb5760863)

这个向上的接口以go channel中的一条消息的形式存在。Raft层会发出这个消息，key-value层要读取这个消息。所以这里有个叫做applyCh的channel，通过它你可以发送ApplyMsg消息。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBML0aSN8VWboRNl1mD%2Fimage.png?alt=media\&token=5f6dde3e-b8b3-4262-b397-32f5a2d24352)

当然，key-value层需要知道从applyCh中读取的消息，对应之前调用的哪个Start函数，所以Start函数的返回需要有足够的信息给key-value层，这样才能完成对应。Start函数的返回值包括，这个请求将会存放在Log中的位置（index）。这个请求不一定能commit成功，但是如果commit成功的话，会存放在这个Log位置。同时，它还会返回当前的任期号（term number）和一些其它我们现在还不太关心的内容。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMNA2u8FqPNWy-T04o%2Fimage.png?alt=media\&token=7f4e769b-62be-41e2-bb22-331610f072f5)

在ApplyMsg中，将会包含请求（command）和对应的Log位置（index）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MBGnNzriBVvlQ9BiPWz%2F-MBMNVDATVlo2e0EeTDf%2Fimage.png?alt=media\&token=65bc08d3-11a8-4a58-b14c-a16e740e6a17)

所有的副本都会收到这个ApplyMsg消息，它们都知道自己应该执行这个请求，弄清楚这个请求的具体含义，并将它应用在本地的状态中。所有的副本节点还会拿到Log的位置信息（index），但是这个位置信息只在Leader有用，因为Leader需要知道ApplyMsg中的请求究竟对应哪个客户端请求（进而响应客户端请求）。

> 学生提问：为什么不在Start函数返回的时候就响应客户端请求呢？
>
> Robert教授：我们假设客户端发送了任意的请求，我们假设这里是一个Put或者Get请求，是什么其实不重要，我们还是假设这里是个Get请求。客户端发送了一个Get请求，并且等待响应。当Leader知道这个请求被（Raft）commit之后，会返回响应给客户端。所以这里会是一个Get响应。所以，（在Leader返回响应之前）客户端看不到任何内容。
>
> 这意味着，在实际的软件中，客户端调用key-value的RPC，key-value层收到RPC之后，会调用Start函数，Start函数会立即返回，但是这时，key-value层不会返回消息给客户端，因为它还没有执行客户端请求，它也不知道这个请求是否会被（Raft）commit。一个不能commit的场景是，当key-value层调用了Start函数，Start函数返回之后，它就故障了，所以它必然没有发送Apply Entry消息或者其他任何消息，所以也不能执行commit。
>
> 所以实际上，Start函数返回了，随着时间的推移，对应于这个客户端请求的ApplyMsg从applyCh channel中出现在了key-value层。只有在那个时候，key-value层才会执行这个请求，并返回响应给客户端。

有一件事情你们需要熟悉，那就是，首先，对于Log来说有一件有意思的事情：不同副本的Log或许不完全一样。有很多场合都会不一样，至少不同副本节点的Log的末尾，会短暂的不同。例如，一个Leader开始发出一轮AppendEntries消息，但是在完全发完之前就故障了。这意味着某些副本收到了这个AppendEntries，并将这条新Log存在本地。而那些没有收到AppendEntries消息的副本，自然也不会将这条新Log存入本地。所以，这里很容易可以看出，不同副本中，Log有时会不一样。

不过对于Raft来说，Raft会最终强制不同副本的Log保持一致。或许会有短暂的不一致，但是长期来看，所有副本的Log会被Leader修改，直到Leader确认它们都是一致的。

接下来会有有关Raft的两个大的主题，一个是Lab2的内容：Leader Election是如何工作的；另一个是，Leader如何处理不同的副本日志的差异，尤其在出现故障之后。
