Files
MIT6.824/lecture-08-zookeeper/8.5.md
2022-01-25 02:41:31 +00:00

10 KiB
Raw Blame History

8.5 一致保证Consistency Guarantees

Zookeeper的确有一些一致性的保证用来帮助那些使用基于Zookeeper开发应用程序的人来理解他们的应用程序以及理解当他们运行程序时会发生什么。与线性一致一样这些保证与序列有关。Zookeeper有两个主要的保证它们在论文的2.3有提及。

第一个是,写请求是线性一致的。

现在你可以发现Zookeeper对于线性一致的定义与我的不太一样因为Zookeeper只考虑写不考虑读。这里的意思是尽管客户端可以并发的发送写请求然后Zookeeper表现的就像以某种顺序一次只执行一个写请求并且也符合写请求的实际时间。所以如果一个写请求在另一个写请求开始前就结束了那么Zookeeper实际上也会先执行第一个写请求再执行第二个写请求。所以这里不包括读请求单独看写请求是线性一致的。Zookeeper并不是一个严格的读写系统。写请求通常也会跟着读请求。对于这种混合的读写请求任何更改状态的操作相比其他更改状态的操作都是线性一致的。

Zookeeper的另一个保证是任何一个客户端的请求都会按照客户端指定的顺序来执行论文里称之为FIFOFirst In First Out客户端序列。

这里的意思是如果一个特定的客户端发送了一个写请求之后是一个读请求或者任意请求那么首先所有的写请求会以这个客户端发送的相对顺序加入到所有客户端的写请求中满足保证1。所以如果一个客户端说先完成这个写操作再完成另一个写操作之后是第三个写操作那么在最终整体的写请求的序列中可以看到这个客户端的写请求以相同顺序出现虽然可能不是相邻的。所以对于写请求最终会以客户端确定的顺序执行。

这里实际上是服务端需要考虑的问题因为客户端是可以发送异步的写请求也就是说客户端可以发送多个写请求给Zookeeper Leader节点而不用等任何一个请求完成。Zookeeper论文并没有明确说明但是可以假设为了让Leader可以实际的按照客户端确定的顺序执行写请求我设想客户端实际上会对它的写请求打上序号表明它先执行这个再执行这个第三个是这个而Zookeeper Leader节点会遵从这个顺序。这里由于有这些异步的写请求变得非常有意思。

对于读请求这里会更加复杂一些。我之前说过读请求不需要经过Leader只有写请求经过Leader读请求只会到达某个副本。所以读请求只能看到那个副本的Log对应的状态。对于读请求我们应该这么考虑FIFO客户端序列客户端会以某种顺序读某个数据之后读第二个数据之后是第三个数据对于那个副本上的Log来说每一个读请求必然要在Log的某个特定的点执行或者说每个读请求都可以在Log一个特定的点观察到对应的状态。

然后后续的读请求必须要在不早于当前读请求对应的Log点执行。也就是一个客户端发起了两个读请求如果第一个读请求在Log中的一个位置执行那么第二个读请求只允许在第一个读请求对应的位置或者更后的位置执行。

第二个读请求不允许看到之前的状态第二个读请求至少要看到第一个读请求的状态。这是一个极其重要的事实我们会用它来实现正确的Zookeeper应用程序。

这里特别有意思的是如果一个客户端正在与一个副本交互客户端发送了一些读请求给这个副本之后这个副本故障了客户端需要将读请求发送给另一个副本。这时尽管客户端切换到了一个新的副本FIFO客户端序列仍然有效。所以这意味着如果你知道在故障前客户端在一个副本执行了一个读请求并看到了对应于Log中这个点的状态

当客户端切换到了一个新的副本并且发起了另一个读请求,假设之前的读请求在这里执行,

那么尽管客户端切换到了一个新的副本客户端的在新的副本的读请求必须在Log这个点或者之后的点执行。

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

那么在获取到对应这个位置的Log之前这个副本不能响应客户端请求。

我不是很清楚这里具体怎么工作,但是要么副本阻塞了对于客户端的响应,要么副本拒绝了客户端的读请求并说:我并不了解这些信息,去问问其他的副本,或者过会再来问我。

最终如果这个副本连上了Leader它会更新上最新的Log到那个时候这个副本就可以响应读请求了。好的所以读请求都是有序的它们的顺序与时间正相关。

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

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

学生提问也就是说从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的一致性保证。