MIT 6.5840(6.824) | ZooKeeper Note

本篇文章为ZooKeeper的学习笔记。

Zookeeper可以被认为是一个通用的协调服务(General-Purpose Coordination Service),通过多副本来完成容错。

性能问题

如果一个读写都通过leader的多副本系统添加服务器,并不能保证性能的提升,相反leader压力增大,性能下降。但如果只将写请求交由leader处理,读请求可以由任一个副本处理,多服务器可以让性能提升。

问题:除了Leader以外的任何一个副本数据不一定是最新(up to date)的:副本不在leader所处的过半服务器中,或者已经完全失联了。

解决方式:ZooKeeper并不要求返回最新的写入数据。Zookeeper的确允许客户端将读请求发送给任意副本,并由副本根据自己的状态来响应读请求。副本的Log可能并没有拥有最新的条目,所以尽管系统中可能有一些更新的数据,这个副本可能还是会返回旧的数据(不提供强一致性)。

一致保证

  1. 写请求线性一致

  2. 任何一个客户端的请求,都会按照客户端指定的顺序来执行,论文里称之为FIFO(First In First Out)客户端序列。

    1. 为了让Leader可以实际的按照客户端确定的顺序执行写请求,我设想,客户端实际上会对它的写请求打上序号,表明它先执行这个,再执行这个,第三个是这个,而Zookeeper Leader节点会遵从这个顺序。这里由于有这些异步的写请求变得非常有意思。

    2. 对于读请求,其不需要经过Leader,只有写请求经过Leader,读请求只会到达某个副本。所以,读请求只能看到那个副本的Log对应的状态。对于读请求,我们应该这么考虑FIFO客户端序列,客户端会以某种顺序读某个数据,之后读第二个数据,之后是第三个数据,对于那个副本上的Log来说,每一个读请求必然要在Log的某个特定的点执行,或者说每个读请求都可以在Log一个特定的点观察到对应的状态。然后,后续的读请求,必须要在不早于当前读请求对应的Log点执行。也就是一个客户端发起了两个读请求,如果第一个读请求在Log中的一个位置执行,那么第二个读请求只允许在第一个读请求对应的位置或者更后的位置执行。第二个读请求不允许看到之前的状态,第二个读请求至少要看到第一个读请求的状态。这是一个极其重要的事实。

    3. 若之前处理读请求的副本发生故障,新副本处理切换来的读请求时,必须在原副本处理上一个请求的log点位或其之后进行。
      此机制的工作原理是,每个Log条目都会被Leader打上zxid的标签,这些标签就是Log对应的条目号。任何时候一个副本回复一个客户端的读请求,首先这个读请求是在Log的某个特定点执行的,其次回复里面会带上zxid,对应的就是Log中执行点的前一条Log条目号。客户端会记住最高的zxid,当客户端发出一个请求到一个相同或者不同的副本时,它会在它的请求中带上这个最高的zxid。这样,其他的副本就知道,应该至少在Log中这个点或者之后执行这个读请求。这里有个有趣的场景,如果第二个副本并没有最新的Log,当它从客户端收到一个请求,客户端说,上一次我的读请求在其他副本Log的这个位置执行,在获取到对应这个位置的Log之前,这个副本不能响应客户端请求。

    4. FIFO客户端请求序列是对一个客户端的所有读请求,写请求生效。所以,如果我发送一个写请求给Leader,在Leader commit这个请求之前需要消耗一些时间,所以我现在给Leader发了一个写请求,而Leader还没有处理完它,或者commit它。之后,我发送了一个读请求给某个副本。这个读请求需要暂缓一下,以确保FIFO客户端请求序列。读请求需要暂缓,直到这个副本发现之前的写请求已经执行了。这是FIFO客户端请求序列的必然结果,(对于某个特定的客户端)读写请求是线性一致的。

      最明显的理解这种行为的方式是,如果一个客户端写了一份数据,例如向Leader发送了一个写请求,之后立即读同一份数据,并将读请求发送给了某一个副本,那么客户端需要看到自己刚刚写入的值。如果我写了某个变量为17,那么我之后读这个变量,返回的不是17,这会很奇怪,这表明系统并没有执行我的请求。因为如果执行了的话,写请求应该在读请求之前执行。所以,副本必然有一些有意思的行为来暂缓客户端,比如当客户端发送一个读请求说,我上一次发送给Leader的写请求对应了zxid是多少,这个副本必须等到自己看到对应zxid的写请求再执行读请求。

从一个副本读取的或许不是最新的数据,所以Leader或许已经向过半服务器发送了C,并commit了,过半服务器也执行了这个请求。但是这个副本并不在Leader的过半服务器中,所以或许这个副本没有最新的数据。这就是Zookeeper的工作方式,它并不保证我们可以看到最新的数据。Zookeeper可以保证读写有序,但是只针对一个客户端来说。所以,如果我发送了一个写请求,之后我读取相同的数据,Zookeeper系统可以保证读请求可以读到我之前写入的数据。但是,如果你发送了一个写请求,之后我读取相同的数据,并没有保证说我可以看到你写入的数据。这就是Zookeeper可以根据副本的数量加速读请求的基础。

同步操作

同步操作可以保证读到最新的数据,用于弥补非严格线性一致。

Zookeeper有一个操作类型是sync,它本质上就是一个写请求。假设我知道你最近写了一些数据,并且我想读出你写入的数据,所以现在的场景是,我想读出Zookeeper中最新的数据。这个时候,我可以发送一个sync请求,它的效果相当于一个写请求,所以它最终会出现在所有副本的Log中,尽管我只关心与我交互的副本,因为我需要从那个副本读出数据。接下来,在发送读请求时,我(客户端)告诉副本,在看到我上一次sync请求之前,不要返回我的读请求。

如果这里把sync看成是一个写请求,这里实际上符合了FIFO客户端请求序列,因为读请求必须至少要看到同一个客户端前一个写请求对应的状态。所以,如果我发送了一个sync请求之后,又发送了一个读请求。Zookeeper必须要向我返回至少是我发送的sync请求对应的状态。

不管怎么样,如果我需要读最新的数据,我需要发送一个sync请求,之后再发送读请求。这个读请求可以保证看到sync对应的状态,所以可以合理的认为是最新的。但是同时也要认识到,这是一个代价很高的操作,因为我们现在将一个廉价的读操作转换成了一个耗费Leader时间的sync操作。所以,如果不是必须的,那还是不要这么做。

就绪文件

To be updated


MIT 6.5840(6.824) | ZooKeeper Note
http://bustdot.github.io/2023/08/31/MIT-6-5840-6-824-ZooKeeper-Note/
作者
BustDot
发布于
2023年8月31日
许可协议