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中写请求可以确保以线性顺序执行。

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

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

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

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

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

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

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

学生提问:听不清

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

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

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

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

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

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

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

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

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

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

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

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

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

学生提问:谁出发了这里的watch?

Robert教授:假设这个客户端与这个副本在交互,它发送了一个exist请求,exist请求是个只读请求。相应的副本在一个table上生成一个watch的表单,表明哪些客户端watch了哪些file。

并且,watch是基于一个特定的zxid建立的,如果客户端在一个副本log的某个位置执行了读请求,并且返回了相对于这个位置的状态,那么watch也是相对于这个位置来进行。如果收到了一个删除Ready file的请求,副本会查看watch表单,并且发现针对这个Ready file有一个watch。watch表单或许是以file名的hash作为key,这样方便查找。

学生提问:这个副本必须要有一个watch表单,如果副本故障了,客户端需要连接到另外一个副本,那新连接的副本中的watch表单如何生成呢?

Robert教授:答案是,如果你的副本故障了,那么切换到的新的副本不会有watch表单。但是客户端在相应的位置会收到通知说,你正在交互的副本故障了,之后客户端就知道,应该重置所有数据,并与新的副本建立连接(包括watch)。

下一节课会继续介绍Zookeeper。

最后更新于