首页 > > > Linearizability 一致性验证
作者:徐鹏
> 上篇文章介绍了 TiDB 如何使用 Jepsen 来进行一致性验证,并且介绍了具体的测试案例,但是并没有对 Jepsen 背后的一致性验证算法做过多介绍。这篇文章将会深入 Jepsen 的核心库 knossos ,介绍 knossos 库所涉及的 Linearizability(线性化)一致性验证算法。
什么是一致性模型?
一致性模型确定了编写系统的程序员与系统之间的某种协议,如果程序员遵守了这种协议,那么这个系统就能提供某种一致性。常见的一致性模型有:
需要注意的是这里的系统指并发系统,分布式系统只是其中的一类。
什么是 Linearizability ?
首先我们需要引入历史(history)的概念,历史是并发系统中由 invocation 事件和 response 事件组成的有限序列。
当我们的历史 H 满足以下条件时我们把它称为顺序化(sequential)历史:
如果一个历史不是循序化的话那么就是并发的。
历史 H 在操作上引出非自反的偏序关系 <H
e0 <H e1 if res(e0) precedes inv(e1) in H
这里的 res 和 inv 分别对应 response 和 invocation。
当历史 H 可以通过增加 >=0 个 response 事件被延长时成为 H' 并且满足以下两个条件时,则这个历史是线性化(linearizable)的。
complete(H') 表示进程以完整的操作进行交互,L2 表示如果 op1 在 H 中先于 op2 存在(注意这里的先于强调实时发生的顺序 real-time order),那么在 S 中也是这样。我们把 S 称为 H 的线性化点(linearization)。
下面我们通过 3 个小例子来解释一下以上 2 个条件。

Linearizability 的性质
正确(correctness)的定义
一段历史 H 由两种对象组成,representation(REP) 和 abstract(ABS) 。abstract 是被实现的类型,而 representation 类型则是用于实现 ABS 的类型。这两种对象在以下条件下进行交互:
对于某个实现中的所有历史 H 来说,如果 H|ABS 是线性化的,那么这个实现就是正确的。
REP 值的子集中的合法表现由表达不变性(representation invariant)所表示:I:REP->BOOL,一个合法表现的含义由抽象函数(abstract function)所表示:A:REP->ABS。对于一个正确的实现 p 来说,存在一个表达不变性 I,以及一个抽象函数 A,并且无论何时从一个合法的表达值 r 到达另一个表达值 r’,抽象操作 a 把抽象值 A(r) 变成 A(r’)。
我们从最简单的队列(FIFO queue)入手。

Enq 和 Deq 可以看做是 abstract operation,而 Enq 和 Deq 中的每条语句可以看做是 representation operation。
对线性化的历史的验证可以被转换为对顺序化历史的验证,对于给定的线性化历史,我们把最终线性化点的对象的值称为线性值。因为给定的历史可能有超过一个线性化点,所以这个对象可能会有多个线性值。我们用表示所有线性值的集合,可以把它们看作是系统外部的观察者所看到的值。
对于以下几个队列操作,对应的线性值分别有以下几种。

为了证明正确性,我们需要保证:
For all r in Lin(H|REP), I(r) holds and A(r) ⊆ Lin(H|ABS)
其中 H|REP 和 H|ABS 都是线性化的,r 代表 H|REP 的线性值,并且 I(r) = (r.back ≥ 1)& (∀ i. i ≥ r.back -> r.elements[i] = null) & (lbound(r.elements) = 1)
其中 lbound 是最小的数组索引(队列从 1 开始)
A(r) = {q | elements(r) = elements(q) & <r ⊆ <q}
其中偏序关系 < r 表示如果被插入元素 x 的赋值操作先于 y 的自增操作,则 x <r y,<q 代表队列 q 的全序关系。
换句话说,队列的表现值(representation value)就是队列中的元素,这些元素的排列顺序与 Enq 操作的顺序一致。
下面这张图可以帮助你很好地理解上述公式的意思。第二列是线性化的表现值(linearized representation values),第三列是线性化的抽象值(linearized abstract values),可以看到每一行中第二列都是第三列的子集。

介绍完了如何证明 linearizability,下面我们可以继续深入到 knossos 使用的两个核心算法之一——Wing & Gong Linearibility 算法(WGL)。
WGL 算法:对于给定的某个数据类型 T,它的并发实现为 ConcObj,而它的顺序化要求为 SeqObj。对于给定的历史 H,我们在保证 H 的实时顺序 <H 的情况下尝试 H 的每一系列可能的顺序化操作,然后检查每个顺序化历史 HS 在 SeqObj 上执行时是否是线性化的。如果 H 的每一种可能都失败了,那么这个历史就不是线性化的。
我们定义历史是由一系列事件组成的:

其中 iterm 是操作 op 的参数,name 是进行操作的进程的名字,prev 和 next 分别表示上一个和下一个事件,match 指向其对应的返回(res)事件。
另外我们还需要区域以及 lift(unlift) 这个概念。
区域(Section):由触发(inv)事件,对应的返回事件,以及它们中间包含的所有事件。

虚线同时可以看作是也是指针。
lift:将某对操作从历史中移出
unlift:将移出的某对操作放回

这个算法的核心是一个搜索(Search)函数,如果历史是线性化的,那么那么他返回一个线性化点(即顺序化历史)。搜索使用一个栈来保存历史中已经线性化的部分,这个栈及栈中的元素是这样定义的:

其中 pi 和 pr 分别表示子历史中第一个没有被检查的区域;inv 和 resp 表示子历史中第一对操作;item、op 和 result 记录这对操作的信息。
一个完整的搜索函数是这样的:
B:1. 如果当前区域内还有一些未被选择的触发(inv)事件没有排在任何返回(res)事件之后,那么选择一个然后回到 4 ; 2. 当前区域的所有操作已经被尝试但是失败了,所以我们需要将操作出栈然后尝试其他的顺序,如果栈是空的,那么意味着历史不是线性化的,函数返回;否则,将顶层元素出栈,这个元素包含了之前区域的所有信息,以及被选择的操作,然后 undo 之前的,unlift 这个操作,最后,设置 current 为之前区域的指针,然后回到 5.B.1
注:4 中操作取决于具体模型,如果被测试的是一个寄存器的话,那可以是 read、write 和 cas,如果和时读到的值和预期值不一致,则操作无法进行。
这就是整个 WGL 算法。这个算法很简单也很好理解,但是有两个明显的缺点:
对此 knossos 库的第二个算法使用了 WGL 算法的改进版本,与 WGL 中的栈存放操作信息不同的是它使用了树遍历和图搜索两种方法来使算法更高效,同时存在 “记忆” 机制来避免对相同的操作进行重复验证,并且如果所验证的历史不满足一致性,会给出具体出错的点。篇幅有限,如果你对这个算法感兴趣的话,文末有链接。
这篇文章介绍了什么是 Linearizability、Linearizability 正确性的验证及其算法。这些算法在分布式系统中的应用只是一个很小的方面,算法本身是独立的,它只需要一个历史 H ,至于这个历史是随机生成的还是某个应用在实际中产生的并不重要。你可以使用这些算法对任何并发系统进行验证,小到一个无锁队列、Set,大到某个分布式系统。TiDB 作为一个分布式数据库却能被抽象化为一个队列、寄存器来被用作测试这本身就是一个很有意思的地方,同时也很好地展现了这些算法自身的魅力。
参考
Consistency Model
https://en.wikipedia.org/wiki/Consistency_model
Knossos
https://github.com/jepsen-io/knossos
Sequential Consistency
http://lamport.azurewebsites.net/pubs/multi.pdf
Linearizability
http://cs.brown.edu/~mph/HerlihyW90/p463-herlihy.pdf
Linearizability versus Serializability
http://www.bailis.org/blog/linearizability-versus-serializability/
WGL 算法
http://www.cs.cmu.edu/~wing/publications/WingGong93.pdf
Testing for Linearizability
http://www.cs.ox.ac.uk/people/gavin.lowe/LinearizabiltyTesting/paper.pdf
原文作者:;如有侵权,请联系管理员删除。