# 8.7 就绪文件（Ready file/znode）

在论文中有几个例子场景，通过Zookeeper的一致性保证可以很简答的解释它们。

首先我想介绍的是论文中2.3有关Ready file的一些设计（_这里的file对应的就是论文里的znode，Zookeeper以文件目录的形式管理数据，所以每一个数据点也可以认为是一个file_）。

我们假设有另外一个分布式系统，这个分布式有一个Master节点，而Master节点在Zookeeper中维护了一个配置，这个配置对应了一些file（也就是znode）。通过这个配置，描述了有关分布式系统的一些信息，例如所有worker的IP地址，或者当前谁是Master。所以，现在Master在更新这个配置，同时，或许有大量的客户端需要读取相应的配置，并且需要发现配置的每一次变化。所以，现在的问题是，尽管配置被分割成了多个file，我们还能有原子效果的更新吗？

为什么要有原子效果的更新呢？因为只有这样，其他的客户端才能读出完整更新的配置，而不是读出更新了一半的配置。这是人们使用Zookeeper管理配置文件时的一个经典场景。

我们这里直接拷贝论文中的2.3节的内容。假设Master做了一系列写请求来更新配置，那么我们的分布式系统中的Master会以这种顺序执行写请求。首先我们假设有一些Ready file，就是以Ready为名字的file。如果Ready file存在，那么允许读这个配置。如果Ready file不存在，那么说明配置正在更新过程中，我们不应该读取配置。所以，如果Master要更新配置，那么第一件事情是删除Ready file。之后它会更新各个保存了配置的Zookeeper file（也就是znode），这里或许有很多的file。当所有组成配置的file都更新完成之后，Master会再次创建Ready file。目前为止，这里的语句都很直观，这里只有写请求，没有读请求，而Zookeeper中写请求可以确保以线性顺序执行。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4cn4IV7ww5YrGiKt_%2Fimage.png?alt=media\&token=ad1f71d3-e8fa-49c3-bd96-f7022788e18a)

为了确保这里的执行顺序，Master以某种方式为这些请求打上了tag，表明了对于这些写请求期望的执行顺序。之后Zookeeper Leader需要按照这个顺序将这些写请求加到多副本的Log中。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4cwZvdgnIEPe1yniK%2Fimage.png?alt=media\&token=93c73bac-9e29-4b87-ad84-75a8c39aed89)

接下来，所有的副本会履行自己的职责，按照这里的顺序一条条执行请求。它们也会删除（自己的）Ready file，之后执行这两个写请求，最后再次创建（自己的）Ready file。所以，这里是写请求，顺序还是很直观的。

对于读请求，需要更多的思考。假设我们有一些worker节点需要读取当前的配置。我们可以假设Worker节点首先会检查Ready file是否存在。如果不存在，那么Worker节点会过一会再重试。所以，我们假设Ready file存在，并且是经历过一次重新创建。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4dXqscEvY4Gy14vni%2Fimage.png?alt=media\&token=613495b9-a0c0-4c7c-961b-0faaf2410570)

这里的意思是，左边的都是发送给Leader的写请求，右边是一个发送给某一个与客户端交互的副本的读请求。之后，如果文件存在，那么客户端会接下来读f1和f2。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4dnXKz2f5-2Fco1uk%2Fimage.png?alt=media\&token=ed644b69-6a37-484a-92ef-9f1fed064963)

这里，有关FIFO客户端序列中有意思的地方是，如果判断Ready file的确存在，那么也是从与客户端交互的那个副本得出的判断。所以，这里通过读请求发现Ready file存在，可以说明那个副本看到了Ready file的重新创建这个请求（由Leader同步过来的）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4eAZpNeDAD5J6_AsP%2Fimage.png?alt=media\&token=5f89254d-aec1-4595-a399-df1a18cbc513)

同时，因为后续的读请求永远不会在更早的log条目号执行，必须在更晚的Log条目号执行，所以，对于与客户端交互的副本来说，如果它的log中包含了这条创建Ready file的log，那么意味着接下来客户端的读请求只会在log中更后面的位置执行（下图中横线位置）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4f-dq24Mw2bYKK2eO%2Fimage.png?alt=media\&token=fb0f7700-d68e-4d68-a217-c8c25375c57f)

所以，如果客户端看见了Ready file，那么副本接下来执行的读请求，会在Ready file重新创建的位置之后执行。这意味着，Zookeeper可以保证这些读请求看到之前对于配置的全部更新。所以，尽管Zookeeper不是完全的线性一致，但是由于写请求是线性一致的，并且读请求是随着时间在Log中单调向前的，我们还是可以得到合理的结果。

> 学生提问：听不清
>
> Robert教授：这是一个很好的问题，你的问题是，在一个实际场景中，会有更多的不确定因素。让我们来看一个更麻烦的场景，这个场景正好我也准备讲。

我们假设Master在完成配置更新之后创建了Ready file。之后Master又要更新配置，那么最开始，它要删除Ready file，之后再执行一些写请求。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4gh5ZvfawnjD8EVmA%2Fimage.png?alt=media\&token=6d344a8e-9491-499a-8846-c63db0b6a1a1)

这里可能有的问题是，需要读取配置的客户端，首先会在这个点，通过调用exist来判断Ready file是否存在。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4gwvRkWWR9tsU_200%2Fimage.png?alt=media\&token=91a3c68d-e4f6-430d-afad-3766d10505db)

在这个时间点，Ready file肯定是存在的。之后，随着时间的推移，客户端读取了组成配置的第一个file，但是，之后在读取第二个file时，Master可能正在更新配置。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4hCfYWzQOi7wI2JXT%2Fimage.png?alt=media\&token=985f4f06-8451-4919-b0a0-c93cb1bbd618)

所以现在客户端读到的是一个不正常的，由旧配置的f1和新配置的f2组成的配置。没有理由相信，这里获取的信息还是有用的。所以，前一个场景还是很美好的，但是这个场景就是个灾难。

所以，我们现在开始面对一个严重的挑战，而一个仔细设计的针对分布式系统中机器间的协调服务的API（就是说Zookeeper），或许可以帮助我们解决这个挑战。对于Lab3来说，你将会构建一个put/get系统，那样一个系统，也会遇到同样的问题，没有任何现有的工具可以解决这个问题。

Zookeeper的API实际上设计的非常巧妙，它可以处理这里的问题。之前说过，客户端会发送exists请求来查询，Ready file是否存在。但是实际上，客户端不仅会查询Ready file是否存在，还会建立一个针对这个Ready file的watch。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4i6qRy1PiLjnuAY94%2Fimage.png?alt=media\&token=80aafde1-c496-485a-a843-d883b02d86a6)

这意味着如果Ready file有任何变更，例如，被删除了，或者它之前不存在然后被创建了，副本会给客户端发送一个通知。在这个场景中，如果Ready file被删除了，副本会给客户端发送一个通知。

客户端在这里只与某个副本交互，所以这里的操作都是由副本完成。当Ready file有变化时，副本会确保，合适的时机返回对于Ready file变化的通知。这里什么意思呢？在这个场景中，这些写请求在实际时间中，出现在读f1和读f2之间。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4ieuIHdqYr1Mzd6po%2Fimage.png?alt=media\&token=65213e56-848d-4415-b07f-7cc178e1f8eb)

而Zookeeper可以保证，如果客户端向某个副本watch了某个Ready file，之后又发送了一些读请求，当这个副本执行了一些会触发watch通知的请求，那么Zookeeper可以确保副本将watch对应的通知，先发给客户端，再处理触发watch通知请求（也就是删除Ready file的请求），在Log中位置之后才执行的读请求（有点绕，后面会有更多的解释）。

这里再来看看Log。FIFO客户端序列要求，每个客户端请求都存在于Log中的某个位置，所以，最后log的相对位置如下图所示：

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4jXc9UYccW3nsX1yO%2Fimage.png?alt=media\&token=e6a35972-b5c5-4341-a7fd-128f3e6e283e)

我们之前已经设置好了watch，Zookeeper可以保证如果某个人删除了Ready file，相应的通知，会在任何后续的读请求之前，发送到客户端。客户端会先收到有关Ready file删除的通知，之后才收到其他在Log中位于删除Ready file之后的读请求的响应。这里意味着，删除Ready file会产生一个通知，而这个通知可以确保在读f2的请求响应之前发送给客户端。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4kccYu9ubDOcG15Ac%2Fimage.png?alt=media\&token=b48421c0-578d-48da-b558-f02bf1242a42)

这意味着，客户端在完成读所有的配置之前，如果对配置有了新的更改，Zookeeper可以保证客户端在收到删除Ready file的通知之前，看到的都是配置更新前的数据（也就是，客户端读取配置读了一半，如果收到了Ready file删除的通知，就可以放弃这次读，再重试读了）。

> 学生提问：谁出发了这里的watch？
>
> Robert教授：假设这个客户端与这个副本在交互，它发送了一个exist请求，exist请求是个只读请求。相应的副本在一个table上生成一个watch的表单，表明哪些客户端watch了哪些file。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD4W2akBJ4UeYmSJJBt%2F-MD4kxVi0CYsgDb34H20%2Fimage.png?alt=media\&token=2107bb3e-dc2e-428b-a794-34f3112c705d)

> 并且，watch是基于一个特定的zxid建立的，如果客户端在一个副本log的某个位置执行了读请求，并且返回了相对于这个位置的状态，那么watch也是相对于这个位置来进行。如果收到了一个删除Ready file的请求，副本会查看watch表单，并且发现针对这个Ready file有一个watch。watch表单或许是以file名的hash作为key，这样方便查找。
>
> 学生提问：这个副本必须要有一个watch表单，如果副本故障了，客户端需要连接到另外一个副本，那新连接的副本中的watch表单如何生成呢？
>
> Robert教授：答案是，如果你的副本故障了，那么切换到的新的副本不会有watch表单。但是客户端在相应的位置会收到通知说，你正在交互的副本故障了，之后客户端就知道，应该重置所有数据，并与新的副本建立连接（包括watch）。

下一节课会继续介绍Zookeeper。
