Lec8 ZooKeeper
Last updated
Last updated
参考链接:
https://zhuanlan.zhihu.com/p/45728390
http://dockone.io/article/9028
https://www.cnblogs.com/biglittleant/p/7770882.html
我们对于正确的定义就是线性一致(Linearizability)或者说强一致(Strong consistency)。通常来说,线性一致等价于强一致。一个服务是线性一致的,那么它表现的就像只有一个服务器,并且服务器没有故障,这个服务器每次执行一个客户端请求,并且没什么奇怪的事情发生。
一个系统的执行历史是一系列的客户端请求,或许这是来自多个客户端的多个请求。如果执行历史整体可以按照一个顺序排列,且排列顺序与客户端请求的实际时间相符合,那么它是线性一致的。
每一个读请求得到的数据都是最近一次写入的值。所有的客户端所经历的都应该是一样的顺序,这才是 linearizability的。
要么我们能构建一个序列,同时满足
序列中的请求的顺序与实际时间匹配
每个读请求看到的都是序列中前一个写请求写入的值
如果我们能构造这么一个序列,那么可以证明,这里的请求历史记录是线性的。
线性一致的一个条件是,对于整个请求历史记录,只存在一个序列,不允许不同的客户端看见不同的序列,或者说不允许一个存储在系统中的数据有不同的演进过程。这里只能有一个序列,所有的客户端必须感受到相同的序列。
线性一致的定义是有关历史记录的定义,而不是系统的定义。所以我们不能说一个系统设计是线性一致的,我们只能说请求的历史记录是线性一致的。如果我们不知道系统内部是如何运作的,我们唯一能做的就是在系统运行的时候观察它,那在观察到任何输出之前,我们并不知道系统是不是线性一致的,我们可以假设它是线性一致的。之后我们看到了越来越多的请求,我们发现,哈,这些请求都满足线性一致的要求,那么我们认为,或许这个系统是线性的。如果我们发现一个请求不满足线性一致的要求,那么这个系统就不是线性一致的。所以是的,线性一致不是有关系统设计的定义,这是有关系统行为的定义。
所以,当你在设计某个东西时,它不那么适用。在设计系统的时候,没有一个方法能将系统设计成线性一致。除非在一个非常简单的系统中,你只有一个服务器,一份数据拷贝,并且没有运行多线程,没有使用多核,在这样一个非常简单的系统中,要想违反线性一致还有点难。但是在任何分布式系统中,又是非常容易违反线性一致性。
线性一致:对于读请求不允许返回旧的数据,只能返回最新的数据。或者说,对于读请求,线性一致系统只能返回最近一次完成的写请求写入的值。
服务器处理重复请求的合理方式是,服务器会根据请求的唯一号或者其他的客户端信息来保存一个表。这样服务器可以记住,哦,我之前看过这个请求,并且执行过它,我会发送一个相同的回复给它,因为我不想执行相同的请求两次。例如,假设这是一个写请求,你不会想要执行这个请求两次。所以,服务器必须要有能力能够过滤出重复的请求。第一个请求的回复可能已经被网络丢包了。所以,服务器也必须要有能力能够将之前发给第一个请求的回复,再次发给第二个重复的请求。所以,服务器记住了最初的回复,并且在客户端重发请求的时候将这个回复返回给客户端。如果服务器这么做了,那么因为服务器或者Leader之前执行第一个读请求的时候,可能看到的是X=3,那么它对于重传的请求,可能还是会返回X=3。所以,我们必须要决定,这是否是一个合法的行为。
这里取决于设计者,但是重传本身是一个底层的行为,或许在RPC的实现里面,或许在一些库里面实现。但是从客户端程序的角度来说,它只知道从第一条竖线的位置发送了一个请求。所以说返回最新的数据或者返回请求时的数据都是ok的。
你们在实验中会完成这样的机制,服务器发现了重复的请求,并将之前的回复重新发给客户端。这里的问题是,服务器最初在这里看到了请求,最后回复的数据是本应在之前一个时间点回复的数据,这样是否合理?我们使用线性一致的定义的一个原因是,它可以用来解释问题。例如,在这个场景里面,我们可以说,这样的行为符合线性一致的原则。
在Dubbo,Kafka,Hadoop等等项目里都能看到ZooKeeper的影子。
ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper的特性:
顺序一致性,从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中去。
原子性,所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,即整个集群要么都成功应用了某个事务,要么都没有应用。
单一视图,无论客户端连接的是哪个 Zookeeper 服务器,其看到的服务端数据模型都是一致的。
可靠性,一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会一直被保留,除非有另一个事务对其进行了变更。
实时性,Zookeeper 保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
ZooKeeper的数据结构:
Zookeeper 使得分布式程序能够通过一个共享的树形结构的名字空间来进行相互协调,即Zookeeper 服务器内存中的数据模型由一系列被称为ZNode的数据节点组成,Zookeeper 将全量的数据存储在内存中,以此来提高服务器吞吐、减少延迟的目的。
Zookeeper 是一个由多个 server 组成的集群,一个 leader,多个 follower。(这个不同于我们常见的Master/Slave模式)leader 为客户端服务器提供读写服务,除了leader外其他的机器只能提供读服务。
每个 server 保存一份数据副本全数据一致,分布式读 follower,写由 leader 实施更新请求转发,由 leader 实施更新请求顺序进行,来自同一个 client 的更新请求按其发送顺序依次执行数据更新原子性,一次数据更新要么成功,要么失败。全局唯一数据视图,client 无论连接到哪个 server,数据视图都是一致的实时性,在一定时间范围内,client 能读到最新数据。(ZooKeeper的leader election也是过半当选)
数据节点Znode:
在Zookeeper中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。
Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。
图1:ZooKeeper分层名称空间的图示。
watcher事件监听器:
ZooKeeper允许用户在指定节点上注册一些Watcher,当数据节点发生变化的时候,ZooKeeper服务器会把这个变化的通知发送给感兴趣的客户端
Paxos 算法可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。
另外,在 ZooKeeper 的官方文档中也指出,ZAB 协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为 ZooKeeper 设计的崩溃可恢复的原子消息广播算法。
ZAB(ZooKeeper Atomic Broadcast 原子广播)协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。
在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播。
当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。
当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。
其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。
当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进人消息广播模式了。
当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播。
那么新加入的服务器就会自觉地进人数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理。
Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议。
而如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器。
zab协议分析:
https://lmx.gitbook.io/mit6.824/mit-6.824-jiang-yi-qi-ta-wen-xian-zi-liao/zookeeper-zab-xie-yi-fen-xi-zhuan
相比Raft来说,Raft实际上就是一个库。你可以在一些更大的多副本系统中使用Raft库。但是Raft不是一个你可以直接交互的独立的服务,你必须要设计你自己的应用程序来与Raft库交互。
所以这里有一个有趣的问题:是否有一些有用的,独立的,通用的系统可以帮助人们构建分布式系统?是否有这样的服务可以包装成一个任何人都可以使用的独立服务,并且极大的减轻构建分布式应用的痛苦?所以,第一个问题是,**对于一个通用的服务,API应该是怎样?**我不太确定类似于Zookeeper这类软件的名字是什么,它们可以被认为是一个通用的协调服务(General-Purpose Coordination Service)。
第二个问题或者说第二个有关Zookeeper的有意思的特性是,作为一个多副本系统,Zookeeper是一个容错的,通用的协调服务,它与其他系统一样,通过多副本来完成容错。所以一个Zookeeper可能有3个、5个或者7个服务器,而这些服务器是要花钱的,例如7个服务器的Zookeeper集群比1个服务器的Zookeeper要贵7倍。所以很自然就会问,如果你买了7个服务器来运行你的多副本服务,你是否能通过这7台服务器得到7倍的性能?我们怎么能达到这一点呢?所以,现在问题是,如果我们有了n倍数量的服务器,是否可以为我们带来n倍的性能?
我会先说一下第二个问题。现在这里讨论的是性能,我接下来将会把Zookeeper看成一个类似于Raft的多副本系统。Zookeeper实际上运行在Zab之上,从我们的角度来看,Zab几乎与Raft是一样的。这里我只看多副本系统的性能,我并不关心Zookeeper的具体功能。
所以,现在全局来看,我们有大量的客户端,或许有数百个客户端,并且我们有一个Leader,这个Leader有两层,上面一层是与客户端交互的Zookeeper,下面是与Raft类似的管理多副本的Zab。Zab所做的工作是维护用来存放一系列操作的Log,这些操作是从客户端发送过来的,这与Raft非常相似。然后会有多个副本,每个副本都有自己的Log,并且会将新的请求加到Log中。这是一个很熟悉的配置(与Raft是一样的)。
当一个客户端发送了一个请求,Zab层会将这个请求的拷贝发送给其他的副本,其他副本会将请求追加在它们的内存中的Log或者是持久化存储在磁盘上,这样它们故障重启之后可以取回这些Log。
当我们加入更多的服务器时,Leader几乎可以确定是一个瓶颈,因为Leader需要处理每一个请求,它需要将每个请求的拷贝发送给每一个其他服务器。当你添加更多的服务器时,你只是为现在的瓶颈(Leader节点)添加了更多的工作负载。所以是的,你并不能通过添加服务器来达到提升性能的目的,因为新增的服务器并没有实际完成任何工作,它们只是愉快的完成Leader交代的工作,它们并没有减少Leader的工作。每一个操作都经过Leader。所以,在这里,随着服务器数量的增加,性能反而会降低,因为Leader需要做的工作更多了。所以,在这个系统中,我们现在有这个问题:更多的服务器使得系统更慢了。
或许最简单的可以用来利用这些服务器的方法,就是构建一个系统,让所有的写请求通过Leader下发。在现实世界中,大量的负载是读请求,也就是说,读请求(比写请求)多得多。比如,web页面,全是通过读请求来生成web页面,并且通常来说,写请求就相对少的多,对于很多系统都是这样的。所以,或许我们可以将写请求发给Leader,但是将读请求发给某一个副本,随便任意一个副本。
如果你有一个读请求,例如Lab3中的get请求,把它发给某一个副本而不是Leader。如果我们这么做了,对于写请求没有什么帮助,是我们将大量的读请求的负担从Leader移走了。现在对于读请求来说,有了很大的提升,因为现在,添加越多的服务器,我们可以支持越多的客户端读请求,因为我们将客户端的读请求分担到了不同的副本上。
Zookeeper作为一个类似于Raft的系统,如果客户端将请求发送给一个随机的副本,那个副本中肯定有一份Log的拷贝,这个拷贝随着Leader的执行而变化。假设在Lab3中,这个副本有一个key-value表,当它收到一个读X的请求,在key-value表中会有X的某个数据,这个副本可以用这个数据返回给客户端。
所以,功能上来说,副本拥有可以响应来自客户端读请求的所有数据。这里的问题是,没有理由可以相信,除了Leader以外的任何一个副本的数据是最新(up to date)的。
这里有很多原因导致副本没有最新的数据,其中一个原因是,这个副本可能不在Leader所在的过半服务器中。对于Raft来说,Leader只会等待它所在的过半服务器中的其他follower对于Leader发送的AppendEntries消息的返回,之后Leader才会commit消息,并进行下一个操作。所以,如果这个副本不在过半服务器中,它或许永远也看不到写请求。又或许网络丢包了,这个副本永远没有收到这个写请求。所以,有可能Leader和过半服务器可以看见前三个请求,但是这个副本只能看见前两个请求,而错过了请求C。所以从这个副本读数据可能读到一个旧的数据。
所以,如果这里不做任何改变,并且我们想构建一个线性一致的系统,尽管在性能上很有吸引力,我们不能将读请求发送给副本,并且你也不应该在Lab3这么做,因为Lab3也应该是线性一致的。这里是线性一致阻止了我们使用副本来服务客户端
实际上,Zookeeper并不要求返回最新的写入数据。Zookeeper的方式是,放弃线性一致性。它对于这里问题的解决方法是,不提供线性一致的读。所以,因此,Zookeeper也不用为读请求提供最新的数据。它有自己有关一致性的定义,而这个定义不是线性一致的,因此允许为读请求返回旧的数据。所以,Zookeeper这里声明,自己最开始就不支持线性一致性,来解决这里的技术问题。如果不提供这个能力,那么(为读请求返回旧数据)就不是一个bug。这实际上是一种经典的解决性能和强一致之间矛盾的方法,也就是不提供强一致。
Zookeeper的确允许客户端将读请求发送给任意副本,并由副本根据自己的状态来响应读请求。副本的Log可能并没有拥有最新的条目,所以尽管系统中可能有一些更新的数据,这个副本可能还是会返回旧的数据。
Zookeeper的确有一些一致性的保证,用来帮助那些使用基于Zookeeper开发应用程序的人,来理解他们的应用程序,以及理解当他们运行程序时,会发生什么。与线性一致一样,这些保证与序列有关。Zookeeper有两个主要的保证,它们在论文的2.3有提及。
ZooKeeper具有两个基本的顺序保证:
Linearizable writes:所有更新ZooKeeper状态的请求都是可线性化的,并且遵循优先级;
FIFO client order:来自给定客户端的所有请求均按照客户端发送的顺序执行。
第一个是,写请求是线性一致的。
现在,你可以发现,它(Zookeeper)对于线性一致的定义与我的不太一样,因为Zookeeper只考虑写,不考虑读。这里的意思是,尽管客户端可以并发的发送写请求,然后Zookeeper表现的就像以某种顺序,一次只执行一个写请求,并且也符合写请求的实际时间。所以如果一个写请求在另一个写请求开始前就结束了,那么Zookeeper实际上也会先执行第一个写请求,再执行第二个写请求。所以,这里不包括读请求,单独看写请求是线性一致的。Zookeeper并不是一个严格的读写系统。写请求通常也会跟着读请求。对于这种混合的读写请求,任何更改状态的操作相比其他更改状态的操作,都是线性一致的。
Zookeeper的另一个保证是,任何一个客户端的请求,都会按照客户端指定的顺序来执行,论文里称之为FIFO(First In First Out)客户端序列。
这里的意思是,如果一个特定的客户端发送了一个写请求之后是一个读请求或者任意请求,那么首先,所有的写请求会以这个客户端发送的相对顺序,加入到所有客户端的写请求中(满足保证1)。所以,如果一个客户端说,先完成这个写操作,再完成另一个写操作,之后是第三个写操作,那么在最终整体的写请求的序列中,可以看到这个客户端的写请求以相同顺序出现(虽然可能不是相邻的)。所以,对于写请求,最终会以客户端确定的顺序执行。(这就保证了对于一个客户端来说是线性一致的,就像你写x=3,然后更新x=2,然后会读得x=2,而不会读到x=3这种怪事。但是别人来读x,很可能就会读到x=3。)
每个Log条目都会被Leader打上zxid的标签,这些标签就是Log对应的条目号。任何时候一个副本回复一个客户端的读请求,首先这个读请求是在Log的某个特定点执行的,其次回复里面会带上zxid,对应的就是Log中执行点的前一条Log条目号。客户端会记住最高的zxid,当客户端发出一个请求到一个相同或者不同的副本时,它会在它的请求中带上这个最高的zxid。这样,其他的副本就知道,应该至少在Log中这个点或者之后执行这个读请求。这里有个有趣的场景,如果第二个副本并没有最新的Log,当它从客户端收到一个请求,客户端说,上一次我的读请求在其他副本Log的这个位置执行,那么在获取到对应这个位置的Log之前,这个副本不能响应客户端请求。
Q&A
学生提问:也就是说,从Zookeeper读到的数据不能保证是最新的?
Robert教授:完全正确。我认为你说的是,从一个副本读取的或许不是最新的数据,所以Leader或许已经向过半服务器发送了C,并commit了,过半服务器也执行了这个请求。但是这个副本并不在Leader的过半服务器中,所以或许这个副本没有最新的数据。这就是Zookeeper的工作方式,它并不保证我们可以看到最新的数据。Zookeeper可以保证读写有序,但是只针对一个客户端来说。所以,如果我发送了一个写请求,之后我读取相同的数据,Zookeeper系统可以保证读请求可以读到我之前写入的数据。但是,如果你发送了一个写请求,之后我读取相同的数据,并没有保证说我可以看到你写入的数据。这就是Zookeeper可以根据副本的数量加速读请求的基础。
学生提问:那么Zookeeper究竟是不是线性一致呢?
Robert教授:我认为Zookeeper不是线性一致的,但是又不是完全的非线性一致。首先,所有客户端发送的请求以一个特定的序列执行,所以,某种意义上来说,所有的写请求是线性一致的。同时,每一个客户端的所有请求或许也可以认为是线性一致的。尽管我不是很确定,Zookeeper的一致性保证的第二条可以理解为,单个客户端的请求是线性一致的。
学生提问:zxid必须要等到写请求执行完成才返回吗?
Robert教授:实际上,我不知道它具体怎么工作,但是这是个合理的假设。当我发送了异步的写请求,系统并没有执行这些请求,但是系统会回复我说,好的,我收到了你的写请求,如果它最后commit了,这将会是对应的zxid。所以这里是一个合理的假设,我实际上不知道这里怎么工作。之后如果客户端执行读请求,就可以告诉一个副本说,这个zxid是我之前发送的一个写请求。
学生提问:Log中的zxid怎么反应到key-value数据库的状态呢?
Robert教授:如果你向一个副本发送读请求,理论上,客户端会认为副本返回的实际上是Table中的值。所以,客户端说,我只想从这个Table读这一行,这个副本会将其当前状态中Table中对应的值和上次更新Table的zxid返回给客户端。
我不太确定,这里有两种可能,我认为任何一种都可以。第一个是,每个服务器可以跟踪修改每一行Table数值的写请求对应的zxid(这样可以读哪一行就返回相应的zxid);另一个是,服务器可以为所有的读请求返回Log中最近一次commit的zxid,不论最近一次请求是不是更新了当前读取的Table中的行。因为,我们只需要确认客户端请求在Log中的执行点是一直向前推进,所以对于读请求,我们只需要返回大于修改了Table中对应行的写请求对应的zxid即可。
我们还有一个问题,是否可能基于这些保证实现合理的编程?总的来说,Zookeeper的一致性保证没有线性一致那么好。尽管它们有一些难以理解,并且需要一些额外共识,例如,读请求可能会返回旧数据,而这在一个线性一致系统不可能发生,但是,这些保证已经足够好了,好到可以用来直观解释很多基于Zookeeper的系统。接下来,我会尝试构建一些例子来解释,为什么Zookeeper不是一个坏的编程模型?
其中一个原因是,有一个弥补(非严格线性一致)的方法。
Zookeeper有一个操作类型是sync,它本质上就是一个写请求。假设我知道你最近写了一些数据,并且我想读出你写入的数据,所以现在的场景是,我想读出Zookeeper中最新的数据。这个时候,我可以发送一个sync请求,它的效果相当于一个写请求,所以它最终会出现在所有副本的Log中,尽管我只关心与我交互的副本,因为我需要从那个副本读出数据。接下来,在发送读请求时,我(客户端)告诉副本,在看到我上一次sync请求之前,不要返回我的读请求。
如果这里把sync看成是一个写请求,这里实际上符合了FIFO客户端请求序列,因为读请求必须至少要看到同一个客户端前一个写请求对应的状态。所以,如果我发送了一个sync请求之后,又发送了一个读请求。Zookeeper必须要向我返回至少是我发送的sync请求对应的状态。
不管怎么样,如果我需要读最新的数据,我需要发送一个sync请求,之后再发送读请求。这个读请求可以保证看到sync对应的状态,所以可以合理的认为是最新的。但是同时也要认识到,这是一个代价很高的操作,因为我们现在将一个廉价的读操作转换成了一个耗费Leader时间的sync操作。所以,如果不是必须的,那还是不要这么做。
这里涉及到ZooKeeper watch的操作
首先我想介绍的是论文中2.3有关Ready file的一些设计(这里的file对应的就是论文里的znode,Zookeeper以文件目录的形式管理数据,所以每一个数据点也可以认为是一个file)。
我们假设有另外一个分布式系统,这个分布式有一个Master节点,而Master节点在Zookeeper中维护了一个配置,这个配置对应了一些file(也就是znode)。通过这个配置,描述了有关分布式系统的一些信息,例如所有worker的IP地址,或者当前谁是Master。所以,现在Master在更新这个配置,同时,或许有大量的客户端需要读取相应的配置,并且需要发现配置的每一次变化。所以,现在的问题是,尽管配置被分割成了多个file,我们还能有原子效果的更新吗?
为什么要有原子效果的更新呢?因为只有这样,其他的客户端才能读出完整更新的配置,而不是读出更新了一半的配置。这是人们使用Zookeeper管理配置文件时的一个经典场景。
假设更新ready file 的步骤如下:为了确保这里的执行顺序,Master以某种方式为这些请求打上了tag,表明了对于这些写请求期望的执行顺序。之后Zookeeper Leader需要按照这个顺序将这些写请求加到多副本的Log中。
接下来,所有的副本会履行自己的职责,按照这里的顺序一条条执行请求。它们也会删除(自己的)Ready file,之后执行这两个写请求,最后再次创建(自己的)Ready file。所以,这里是写请求,顺序还是很直观的。
对于读请求,需要更多的思考。假设我们有一些worker节点需要读取当前的配置。我们可以假设Worker节点首先会检查Ready file是否存在。如果不存在,那么Worker节点会过一会再重试。所以,我们假设Ready file存在,并且是经历过一次重新创建。
如果出现下面这个请求:
那么现在客户端读到的是一个不正常的,由旧配置的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中位置之后才执行的读请求(有点绕,后面会有更多的解释)。
我们之前已经设置好了watch,Zookeeper可以保证如果某个人删除了Ready file,相应的通知,会在任何后续的读请求之前,发送到客户端。客户端会先收到有关Ready file删除的通知,之后才收到其他在Log中位于删除Ready file之后的读请求的响应。这里意味着,删除Ready file会产生一个通知,而这个通知可以确保在读f2的请求响应之前发送给客户端。
这意味着,客户端在完成读所有的配置之前,如果对配置有了新的更改,Zookeeper可以保证客户端在收到删除Ready file的通知之前,看到的都是配置更新前的数据(也就是,客户端读取配置读了一半,如果收到了Ready file删除的通知,就可以放弃这次读,再重试读了)。
Q&A
学生提问:谁触发了这里的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)。