> 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-04-vmware-ft/4.4-non-determistic.md).

# 4.4 非确定性事件（Non-Deterministic Events）

好的，目前为止，我们都假设只要Backup虚机也看到了来自客户端的请求，经过同样的执行过程，那么它就会与Primary保持一致，但是这背后其实有很多很重要的细节。就如其他同学之前指出的一样，其中一个问题是存在非确定性（Non-Deterministic）的事件。虽然通常情况下，代码执行都是直接明了的，但并不是说计算机中每一个指令都是由计算机内存的内容而确定的行为。这一节，我们来看一下不由当前内存直接决定的指令。如果我们不够小心，这些指令在Primary和Backup的运行结果可能会不一样。这些指令就是所谓的非确定性事件。所以，设计者们需要弄明白怎么让这一类事件能在Primary和Backup之间同步。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDyiqxg-s1TjskYCIgH%2F-ME-yXSE8rnBsxYOgXCE%2Fimage.png?alt=media\&token=b40296a7-57ee-40d1-97e7-2efdbd7f447d)

非确定性事件可以分成几类。

* 客户端输入。假设有一个来自于客户端的输入，这个输入随时可能会送达，所以它是不可预期的。客户端请求何时送达，会有什么样的内容，并不取决于服务当前的状态。我们讨论的系统专注于通过网络来进行交互，所以这里的系统输入的唯一格式就是网络数据包。所以当我们说输入的时候，我们实际上是指接收到了一个网络数据包。而一个网络数据包对于我们来说有两部分，一个是数据包中的数据，另一个是提示数据包送达了的中断。当网络数据包送达时，通常网卡的DMA（Direct Memory Access）会将网络数据包的内容拷贝到内存，之后触发一个中断。操作系统会在处理指令的过程中消费这个中断。对于Primary和Backup来说，这里的步骤必须看起来是一样的，否则它们在执行指令的时候就会出现不一致。所以，这里的问题是，中断在什么时候，具体在指令流中的哪个位置触发？对于Primary和Backup，最好要在相同的时间，相同的位置触发，否则执行过程就是不一样的，进而会导致它们的状态产生偏差。所以，我们不仅关心网络数据包的内容，还关心中断的时间。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDyiqxg-s1TjskYCIgH%2F-ME02D81ATmMYWIFWy8S%2Fimage.png?alt=media\&token=4eaff3ef-6216-4e3a-8986-bc6bb45bbddf)

* 另外，如其他同学指出的，有一些指令在不同的计算机上的行为是不一样的，这一类指令称为怪异指令，比如说：
  * 随机数生成器
  * 获取当前时间的指令，在不同时间调用会得到不同的结果
  * 获取计算机的唯一ID

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDyiqxg-s1TjskYCIgH%2F-ME03ZiYp-zkSFfqkBMO%2Fimage.png?alt=media\&token=64db2c12-3879-43a9-b650-2785fa0eb02c)

* 另外一个常见的非确定事件，在VMware FT论文中没有讨论，就是多CPU的并发。我们现在讨论的都是一个单进程系统，没有多CPU多核这种事情。之所以多核会导致非确定性事件，是因为当服务运行在多CPU上时，指令在不同的CPU上会交织在一起运行，进而产生的指令顺序是不可预期的。所以如果我们在Backup上运行相同的代码，并且代码并行运行在多核CPU上，硬件会使得指令以不同（于Primary）的方式交织在一起，而这会引起不同的运行结果。假设两个核同时向同一份数据请求锁，在Primary上，核1得到了锁；在Backup上，由于细微的时间差别核2得到了锁，那么执行结果极有可能完全不一样，这里其实说的就是（在两个副本上）不同的线程获得了锁。所以，多核是一个巨大的非确定性事件来源，VMware FT论文完全没有讨论它，并且它也不适用与我们这节课的讨论。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDyiqxg-s1TjskYCIgH%2F-ME04mgjGhwX1sgSK6hG%2Fimage.png?alt=media\&token=172b2884-e6c2-43c0-8cd7-d42de7c89e62)

> 学生提问：如何确保VMware FT管理的服务只使用单核？
>
> Robert教授：服务不能使用多核并行计算。硬件几乎可以肯定是多核并行的，但是这些硬件在VMM之下。在这篇论文中，VMM暴露给运行了Primary和Backup虚机操作系统的硬件是单核的。我猜他们也没有一种简单的方法可以将这里的内容应用到一个多核的虚拟机中。

所有的事件都需要通过Log Channel，从Primary同步到Backup。有关日志条目的格式在论文中没有怎么描述，但是我（Robert教授）猜日志条目中有三样东西：

1. 事件发生时的指令序号。因为如果要同步中断或者客户端输入数据，最好是Primary和Backup在相同的指令位置看到数据，所以我们需要知道指令序号。这里的指令号是自机器启动以来指令的相对序号，而不是指令在内存中的地址。比如说，我们正在执行第40亿零79条指令。所以日志条目需要有指令序号。对于中断和输入来说，指令序号就是指令或者中断在Primary中执行的位置。对于怪异的指令（Weird instructions），比如说获取当前的时间来说，这个序号就是获取时间这条指令执行的序号。这样，Backup虚机就知道在哪个指令位置让相应的事件发生。
2. 日志条目的类型，可能是普通的网络数据输入，也可能是怪异指令。
3. 最后是数据。如果是一个网络数据包，那么数据就是网络数据包的内容。如果是一个怪异指令，数据将会是这些怪异指令在Primary上执行的结果。这样Backup虚机就可以伪造指令，并提供与Primary相同的结果。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-ME2qZLtaIXRRriqAUna%2F-ME2tefaQ4OhWCjbXcAz%2Fimage.png?alt=media\&token=4de7d2e4-85c2-45bb-b76f-f5d5bcab3265)

举个例子，Primary和Backup两个虚机内部的guest操作系统需要在模拟的硬件里有一个定时器，能够每秒触发100次中断，这样操作系统才可以通过对这些中断进行计数来跟踪时间。因此，这里的定时器必须在Primary和Backup虚机的完全相同位置产生中断，否则这两个虚机不会以相同的顺序执行指令，进而可能会产生分歧。所以，在运行了Primary虚机的物理服务器上，有一个定时器，这个定时器会计时，生成定时器中断并发送给VMM。在适当的时候，VMM会停止Primary虚机的指令执行，并记下当前的指令序号，然后在指令序号的位置插入伪造的模拟定时器中断，并恢复Primary虚机的运行。之后，VMM将指令序号和定时器中断再发送给Backup虚机。虽然Backup虚机的VMM也可以从自己的物理定时器接收中断，但是它并没有将这些物理定时器中断传递给Backup虚机的guest操作系统，而是直接忽略它们。当来自于Primary虚机的Log条目到达时，Backup虚机的VMM配合特殊的CPU特性支持，会使得物理服务器在相同的指令序号处产生一个定时器中断，之后VMM获取到这个中断，并伪造一个假的定时器中断，并将其送入Backup虚机的guest操作系统，并且这个定时器中断会出现在与Primary相同的指令序号位置。

> 学生提问：这里的操作依赖硬件的定制吗？（实际上我听不清，猜的）
>
> Robert教授：是的，这里依赖于CPU的一些特殊的定制，这样VMM就可以告诉CPU，执行1000条指令之后暂停一下，方便VMM将伪造的中断注入，这样Backup虚机就可以与Primary虚机在相同的指令位置触发相同的中断，执行相同的指令。之后，VMM会告诉CPU恢复执行。这里需要一些特殊的硬件，但是现在看起来所有的Intel芯片上都有这个功能，所以也不是那么的特殊。或许15年前，这个功能还是比较新鲜的，但是现在来说就比较正常了。现在这个功能还有很多其他用途，比如说做CPU时间性能分析，可以让处理器每1000条指令中断一次，这里用的是相同的硬件让微处理器每1000条指令产生一个中断。所以现在，这是CPU中非常常见的一个小工具。
>
> 学生提问：如果Backup领先了Primary会怎么样？
>
> Robert教授： 场景可能是这样，Primary即将在第100万条指令处中断，但是Backup已经执行了100万零1条指令了。如果我们让这种场景发生，那么Primary的中断传输就太晚了。如果我们允许Backup执行领先Primary，就会使得中断在Backup中执行位置落后于Primary。所以我们不能允许这种情况发生，我们不能允许Backup在执行指令时领先于Primary。
>
> VMware FT是这么做的。它会维护一个来自于Primary的Log条目的等待缓冲区，如果缓冲区为空，Backup是不允许执行指令的。如果缓冲区不为空，那么它可以根据Log的信息知道Primary对应的指令序号，并且会强制Backup虚机最多执行指令到这个位置。所以，Backup虚机的CPU总是会被通知执行到特定的位置就停止。Backup虚机只有在Log缓冲区中有数据才会执行，并且只会执行到Log条目对应的指令序号。在Primary产生的第一个Log，并且送达Backup之前，Backup甚至都不能执行指令，所以Backup总是落后于Primary至少一个Log。如果物理服务器的资源占用过多，导致Backup执行变慢，那么Backup可能落后于Primary多个Log条目。

网络数据包送达时，有一个细节会比较复杂。当网络数据包到达网卡时，如果我们没有运行虚拟机，网卡会将网络数据包通过DMA的方式送到计算机的关联内存中。现在我们有了虚拟机，并且这个网络数据包是发送给虚拟机的，在虚拟机内的操作系统可能会监听DMA并将数据拷贝到虚拟机的内存中。因为VMware的虚拟机设计成可以支持任何操作系统，我们并不知道网络数据包到达时操作系统会执行什么样的操作，有的操作系统或许会真的监听网络数据包拷贝到内存的操作。

我们不能允许这种情况发生。如果我们允许网卡直接将网络数据包DMA到Primary虚机中，我们就失去了对于Primary虚机的时序控制，因为我们也不知道什么时候Primary会收到网络数据包。所以，实际中，物理服务器的网卡会将网络数据包拷贝给VMM的内存，之后，网卡中断会送给VMM，并说，一个网络数据包送达了。这时，VMM会暂停Primary虚机，记住当前的指令序号，将整个网络数据包拷贝给Primary虚机的内存，之后模拟一个网卡中断发送给Primary虚机。同时，将网络数据包和指令序号发送给Backup。Backup虚机的VMM也会在对应的指令序号暂停Backup虚机，将网络数据包拷贝给Backup虚机，之后在相同的指令序号位置模拟一个网卡中断发送给Backup虚机。这就是论文中介绍的Bounce Buffer机制。

> 学生提问：怪异的指令（Weird instructions）会有多少呢？
>
> Robert教授：怪异指令非常少。只有可能在Primary和Backup中产生不同结果的指令，才会被封装成怪异指令，比如获取当前时间，或者获取当前处理器序号，或者获取已经执行的的指令数，或者向硬件请求一个随机数用来加密，这种指令相对来说都很少见。大部分指令都是类似于ADD这样的指令，它们会在Primary和Backup中得到相同的结果。每个网络数据包未做修改直接被打包转发，然后被两边虚拟机的TCP/IP协议栈解析也会得到相同的结果。所以我预期99.99%的Log Channel中的数据都会是网络数据包，只有一小部分是怪异指令。
>
> 所以对于一个服务于客户端的服务来说，我们可以通过客户端流量判断Log Channel的流量大概是什么样子，因为它基本上就是客户端发送的网络数据包的拷贝。
