8.1 线性一致(Linearizability)(1)

上一节课,我对线性一致这个概念开了个头,这一次我们来讲完它。

之所以我们要再进一步介绍这个概念,是因为这是我们对于存储系统中强一致的一种标准定义。例如,你们在Lab3中实现的系统必须是线性一致的。有时,当我们在讨论一个强一致的系统时,我们会想知道一个特定的行为是否是可接受的。其他时候,例如,当我们在讨论一个非线性一致的系统时,我们可能会想知道系统会以什么方式偏离线性一致。所以,首先,你需要能够查看某个系统的执行历史记录,并且回答这个问题:刚刚查看的操作的序列是否是线性一致的?接下来我会继续分析,并构建几个有趣的例子来帮助我们理解线性一致系统的响应。

线性一致是特定的操作历史记录的特性。所以,我们总是会提到,我们观察到了一系列不同时间的客户端请求和这些请求的响应,它们请求不同的数据,并且得到了各种各样的回复,我们需要回答,这样的一个历史记录是不是线性的?

下面会介绍一个历史记录的例子,它或许是线性的,或许不是。我们用一个图表示这个例子,在图里面,越靠右,时间越靠后。同时我们有一些客户端。这里的竖线表示客户端发送了一个请求,并且这是个写请求,它将key为X的数据的值写成0。所以这里有一个key,一个value,并且请求对应于将key为X的数据设置成0的PUT操作。

这是我们观察到的结果,客户端发出请求到我们的服务,某个时间点,服务响应了并说,好的,你的写操作完成了。

所以我们假设这里的服务具备通知请求完成的能力,否则我们很难判断线性一致。所以,我们有了某人发出的这个写请求。在这个例子中,我假设还有另一个请求。这根竖线意味着第二个请求在第一个请求结束之后开始。

这一点之所以重要的原因是,线性一致的历史记录必须与请求的实际时间匹配。这里的真实意思是在实际时间中,某个请求如果在另一个请求结束之后才开始,那么在我们构建用于证明线性一致的序列中,后来的请求都必须在先来的请求之后。

所以,在这里例子中,我假设有另一个写X的请求,将X写成1。

之后有个并发的写请求,或许比前一个请求开始的稍晚一点,将X写成2。

这里我们有两个客户端,在差不多的时间发送了两个不同的请求,想将X设置成两个不同的值。所以,当然,我们想知道最后X会是哪个值?之后,我们还有一些读操作。当你只有一些写操作时,很难判断线性一致,因为你没有任何证据证明系统实际做了哪些操作,或者存储了什么数据,所以(在判断线性一致时)我们必须要有一些读操作。

我们假设有一些读操作,其中一个读操作,在第一条竖线发起,在第二条竖线得到回复。这个操作读的是key X,得到的是2。

之后,有来自于同一个客户端的另一个读请求,但是这个请求在前一个读请求结束之后才开始,第二个读X的请求得到1。

所以,我们面前现在有个问题,这个历史记录是不是线性一致的?这里有两种可能。

要么我们能构建一个序列,同时满足

  1. 序列中的请求的顺序与实际时间匹配

  2. 每个读请求看到的都是序列中前一个写请求写入的值

如果我们能构造这么一个序列,那么可以证明,这里的请求历史记录是线性的。

另一种可能是,如果将上面的规则应用之后生成了一个带环的图,那么证明请求历史记录不是线性一致的。对于小规模的历史记录,我们可以遍历每个请求来做判断。

那么这里的请求历史记录是线性一致的吗?

学生回答1(Robert教授复述):这里的回答是,这里有点麻烦。我们看到读X得到2,之后读X得到1,或许这里会自相矛盾。

因为这里有两个写请求,一个写入1,另一个写入2。如果我们读X得到了3,那明显是个很糟糕的错误。但是现在有写X为1和2的请求,并且我们读到的X也是1和2。所以,这里的问题是,这里的读请求的顺序是否会与请求历史记录中的两个写请求矛盾?

学生回答2(Robert教授复述答案):在这里,我们或许有2个或者3个客户端,它们与某个服务交互,或许是个Raft服务。我们能看到的只有请求和响应。这里的意思是,我们看到了一个客户端请求写X为1。

我们在这里看到了响应。所以我们知道,在这个区间里的某处,服务实际上在内部将X的值改为1。

这里的意思是,在这个区间中的某处,服务在内部将X的值改为2。这里可能是区间里的任意一个时间点。这回答了你的问题吗?

学生回答3(Robert教授复述答案):所以这里的回答是,这里有实际的证据证明是线性一致的,也就是说有一个序列表明它是线性一致的。

所以是的,这里的请求历史记录是线性一致的。这里的序列是,首先是将X写0的请求,之后服务器收到了两个差不多时间的写操作,服务自己要为这两个写操作挑一个顺序。所以,我们可以假设,服务器先执行了将X写2的请求,之后执行读X返回2的请求,也就是第一个读X的请求。下一个请求是将X写1的请求,最后一个请求是读X返回1。

所以,这就是证明这里的请求历史是线性一致的证据,因为这个序列有所有请求,并且这个序列匹配请求的实际时间。

我们来再过一遍所有的请求。将X写0的请求在最开始,因为它在所有其他操作开始之前就结束了。我们将X写2的请求排第二。这里我会标记请求在实际时间中生效的位置,我用一个大X来标记这个请求实际发生的时间。所以,第二个请求的实际生效时间在这里。

下一个请求是读X得到2。这里并没有时间上的问题,因为读X得到2实际上与写X为2,这两个请求是并发的。这里并不是读X得到2结束之后,写X为2的请求才开始,这里它们是并发的。我们假设读X得到2的请求实际发生在这里。

我们并不关心第一个请求在什么时候发生。现在我们有了前3个请求的执行时间。

之后我们有个请求将X写为1,我们假设它在实际时间中发生在这里,因为它必须在序列中的前一个请求之后发生。所以,这是第4个请求。

之后,我们有读X得到1的请求,它可能在任何时间发生,但是让我们假设它发生在这里。

所以,这里展示了一个与实际时间匹配的序列,我们可以为每一个请求,在其开始和结束时间的区间里面挑选一个时间,来执行这个请求,挑选的时间可以匹配请求的实际时间。

所以这里的最后一个问题是,每个读操作是不是看到了前一个写请求写入的值?这里的读X得到2的请求,在写X为2的请求之后,这没问题。

读X得到1的请求,紧跟在在写X为1的请求之后。

所以这里的历史记录是线性一致的。

并不是所有的请求历史记录都能直接明了的判断是否是线性一致的。当看到这个例子里的历史记录时,很容易被误导。比如,写X为1的请求(比写X为2的请求)先开始,所以我们就假设X会先被写成1,但是在实际中不一定是这样的。

大家有什么问题吗?

学生提问:如果将写X为2的开始时间改在读X为2的结束时间之后会怎样?

Robert教授:如果写X为2的请求在读X得到2的请求结束之后才开始,那就不是线性一致了。因为在任何我们构建的序列中,都必须要遵守实际时间的顺序。同时,因为在上面的例子中,因为我们没有其他的写X为2的请求,这意味着这里的读请求只能得到0或者1,因为这里是其他两个可能在这个读请求之前的写请求。所以,修改之后,这里的例子就不再是线性一致的了。

学生提问:所以这里完全是根据客户端看到的响应来判断?

Robert教授:是的,所以这(线性一致)是一个非常以客户端为中心的定义,它表明客户端应该看到怎样的请求顺序。但是这背后发生了什么,或许服务有大量的副本,或许是一个复杂的网络,谁知道呢?这些基本与我们无关。这里的定义只关心客户端看到了什么。这里有一些灰度空间,我后面会介绍。例如,我们需要考虑,客户端可能需要重传一个请求。

(下面的内容在视频中时间不连续,是在讲解其他例子的时候,学生对这个例子的提问,因为内容相关,就放到这里)

学生提问:也就是收,系统可以在一个请求区间的任意时间点执行请求?

Robert教授:是的,如果请求的区间有重合,那么系统可以在区间的任何时间点执行请求,所以系统可能以任何的顺序执行这些请求。现在,你知道,如果不是这里的两个读请求,那么系统可以自由的以任何顺序执行这些写请求。但是因为现在我们看到了这两个读请求,我们知道了唯一的合法的顺序是先写X为2的请求,之后是写X为1的请求。所以是的,如果这里的两个读请求是重叠的,那么这两个读请求可以是任意的执行顺序。实际上,直到我们看见读请求返回了2和1,系统在commit之前可以以任意顺序返回读请求的数值。

学生提问:线性一致和强一致的区别是什么?

Robert教授:我将它们(线性一致和强一致)看成同义词。对于大部分的论文,尽管最近的论文可能不太一样,线性一致有明确的定义。人们对于线性一致的定义实际上没有相差太多,但是,对于强一致的具体定义来说,我认为共识会少一些。通常来说,它的定义与线性一致的定义非常接近。例如,强一致系统表现的也与系统中只有一份数据的拷贝一样,这与线性一致的定义非常接近。所以,可以合理的认为强一致与线性一致是一样的。

最后更新于