# 4.5 输出控制（Output Rule）

对于VMware FT系统的输出，也是值得说一下的。在这个系统中，唯一的输出就是对于客户端请求的响应。客户端通过网络数据包将数据送入，服务器的回复也会以网络数据包的形式送出。我之前说过，Primary和Backup虚机都会生成回复报文，之后通过模拟的网卡送出，但是只有Primary虚机才会真正的将回复送出，而Backup虚机只是将回复简单的丢弃掉。

好吧，真实情况会复杂一些。假设我们正在跑一个简单的数据库服务器，这个服务器支持一个计数器自增操作，工作模式是这样，客户端发送了一个自增的请求，服务器端对计数器加1，并返回新的数值。假设最开始一切正常，在Primary和Backup中的计数器都存了10。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5RcDZye9r6o2_3jwx%2Fimage.png?alt=media\&token=ccb7297d-da84-4759-8ac2-28692d483998)

现在，局域网的一个客户端发送了一个自增的请求给Primary，

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5RkLIno4Oj7CL-GdF%2Fimage.png?alt=media\&token=753e10ae-14ac-429d-b706-0da2bf168ef7)

这个请求在Primary虚机的软件中执行，Primary会发现，现在的数据是10，我要将它变成11，并回复客户端说，现在的数值是11。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5RsnEu834vQIA0oTa%2Fimage.png?alt=media\&token=b0d1331c-5948-4541-b491-d361301eb9fa)

这个请求也会发送给Backup虚机，并将它的数值从10改到11。Backup也会产生一个回复，但是这个回复会被丢弃，这是我们期望发生的。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5S4RfoHgn85-B6_U4%2Fimage.png?alt=media\&token=9852cfaf-b976-45be-b9b1-2579c61e6cb3)

但是，你需要考虑，如果在一个不恰当的时间，出现了故障会怎样？在这门课程中，你需要始终考虑，故障的最坏场景是什么，故障会导致什么结果？在这个例子中，假设Primary确实生成了回复给客户端，但是之后立马崩溃了。更糟糕的是，现在网络不可靠，Primary发送给Backup的Log条目在Primary崩溃时也丢包了。那么现在的状态是，客户端收到了回复说现在的数据是11，但是Backup虚机因为没有看到客户端请求，所以它保存的数据还是10。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5U1V6w6QGqwRodwgd%2Fimage.png?alt=media\&token=1703556f-22bf-46ae-ab25-d1cbbac0d097)

现在，因为察觉到Primary崩溃了，Backup接管服务。这时，客户端再次发送一个自增的请求，这个请求发送到了原来的Backup虚机，它会将自身的数值从10增加到11，并产生第二个数据是11的回复给客户端。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME58-xtdT0q4Bk-qeNS%2F-ME5UgzYVRpznXnp7fKk%2Fimage.png?alt=media\&token=51034f9c-921e-4cb4-aa58-53b1d80aeb05)

如果客户端比较前后两次的回复，会发现一个明显不可能的场景（两次自增的结果都是11）。

因为VMware FT的优势就是在不修改软件，甚至软件都不需要知道复制的存在的前提下，就能支持容错，所以我们也不能修改客户端让它知道因为容错导致的副本切换触发了一些奇怪的事情。在VMware FT场景里，我们没有修改客户端这个选项，因为整个系统只有在不修改服务软件的前提下才有意义。所以，前面的例子是个大问题，我们不能让它实际发生。有人还记得论文里面是如何防止它发生的吗？

论文里的解决方法就是控制输出（Output Rule）。直到Backup虚机确认收到了相应的Log条目，Primary虚机不允许生成任何输出。让我们回到Primary崩溃前，并且计数器的内容还是10，Primary上的正确的流程是这样的：

1. 客户端输入到达Primary。
2. Primary的VMM将输入的拷贝发送给Backup虚机的VMM。所以有关输入的Log条目在Primary虚机生成输出之前，就发往了Backup。之后，这条Log条目通过网络发往Backup，但是过程中有可能丢失。
3. Primary的VMM将输入发送给Primary虚机，Primary虚机生成了输出。现在Primary虚机的里的数据已经变成了11，生成的输出也包含了11。但是VMM不会无条件转发这个输出给客户端。
4. Primary的VMM会等到之前的Log条目都被Backup虚机确认收到了才将输出转发给客户端。所以，包含了客户端输入的Log条目，会从Primary的VMM送到Backup的VMM，Backup的VMM不用等到Backup虚机实际执行这个输入，就会发送一个表明收到了这条Log的ACK报文给Primary的VMM。当Primary的VMM收到了这个ACK，才会将Primary虚机生成的输出转发到网络中。

所以，这里的核心思想是，确保在客户端看到对于请求的响应时，Backup虚机一定也看到了对应的请求，或者说至少在Backup的VMM中缓存了这个请求。这样，我们就不会陷入到这个奇怪的场景：客户端已经收到了回复，但是因为有故障发生和副本切换，新接手的副本完全不知道客户端之前收到了对应的回复。

如果在上面的步骤2中，Log条目通过网络发送给Backup虚机时丢失了，然后Primary虚机崩溃了。因为Log条目丢失了， 所以Backup节点也不会发送ACK消息。所以，如果Log条目的丢失与Primary的崩溃同一时间发生，那么Primary必然在VMM将回复转发到网络之前就崩溃了，所以客户端也就不会收到任何回复，所以客户端就不会观察到任何异常。这就是输出控制（Output rule）。

> 学生提问：VMM这里是具体怎么实现的？
>
> Robert教授：我不太清楚，论文也没有说VMM是如何实现的。我的意思是，这里涉及到非常底层的内容，因为包括了内存分配，页表（page table）分配，设备驱动交互，指令拦截，并理解guest操作系统正在执行的指令。这些都是底层的东西，它们通常用C或者C++实现，但是具体的内容我就不清楚了。

所以，Primary会等到Backup已经有了最新的数据，才会将回复返回给客户端。这几乎是所有的复制方案中对于性能产生伤害的地方。这里的同步等待使得Primary不能超前Backup太多，因为如果Primary超前了并且又故障了，对应的就是Backup的状态落后于客户端的状态。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MEAOE1zIqH2Wx1Ueq0F%2F-MEAfKDjVrSN2zOR6uG9%2Fimage.png?alt=media\&token=2e2d2e4b-3d12-4f03-a529-7c4341f2f109)

所以，几乎每一个复制系统都有这个问题，在某个时间点，Primary必须要停下来等待Backup，这对于性能是实打实的限制。即使副本机器在相邻的机架上，Primary节点发送消息并收到回复仍然需要0.5毫秒的延时。如果我们想要能承受类似于地震或者城市范围内的断电等问题，Primary和Backup需要在不同的城市，之间可能有5毫秒的差距。如果我们将两个副本放置在不同的城市，每次生成一个输出时，都需要至少等待5毫秒，等Backup确认收到了前一个Log条目，然后VMM才能将输出发送到网络。对于一些低请求量的服务，这不是问题。但是如果我们的服务要能够每秒处理数百万个请求，那就会对我们的性能产生巨大的伤害。

所以如果条件允许，人们会更喜欢使用在更高层级做复制的系统（详见4.2 最后两段）。这样的复制系统可以理解操作的含义，这样的话Primary虚机就不必在每个网络数据包暂停同步一下，而是可以在一个更高层级的操作层面暂停来做同步，甚至可以对一些只读操作不做暂停。但是这就需要一些特殊的应用程序层面的复制机制。

> 学生提问：其实不用暂停Primary虚机的执行，只需要阻止Primary虚机的输出就行吧？
>
> Robert教授：你是对的。所以，这里的同步等待或许没有那么糟糕。但是不管怎么样，在一个系统中，本来可以几微秒响应一个客户端请求，而现在我们需要先更新另一个城市的副本，这可能会将一个10微秒的操作变成10毫秒。
>
> 学生提问：这里虽然等待时间比较长，如果提高请求的并发度，是不是还是可以有高性能？
>
> Robert教授：如果你有大量的客户端并发的发送请求，那么你或许还是可以在高延时的情况下获得高的吞吐量，但是就需要你有足够聪明的设计和足够的幸运。
>
> 学生提问：可以不可以将Log保留在Primary虚机对应的物理服务器内存中，这样就不用长时间的等待了。
>
> Robert教授：这是一个很好的想法。但是如果你这么做的话，物理服务器宕机，Log就丢失了。通常，如果服务器故障，就认为服务器中的所有数据都没了，其中包括内存的内容。如果故障是某人不小心将服务器的电源拔了，即使Primary对应的物理服务器有电池供电的RAM，Backup也没办法从其获取Log。实际上，系统会在Backup的内存中记录Log。为了保证系统的可靠性，Primary必须等待Backup的ACK才真正输出。你这里的想法很好，但是我们还是不能使用Primary的内存来存Log。
>
> 学生提问：能不能输入送到Primary，输出从Backup送出？
>
> Robert教授：这是个很聪明的想法。我之前完全没有想到过这点。它或许可以工作，我不确定，但是这很有意思。
