8.5 一致保证(Consistency Guarantees)

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

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

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

Zookeeper的另一个保证是,任何一个客户端的请求,都会按照客户端指定的顺序来执行,论文里称之为FIFO(First 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的一致性保证。

最后更新于