文章目录
Linearizability & Serializability
Linearizability(线性一致性) 可以用来衡量一个分布式系统的正确程度,来自于《Linearizability: A Correctness Condition for Concurrent Objects》一文。
如果一系列的操作并发交叉进行,,最终形成的history(可以理解为运行记录),与顺序执行这些操作形成的sequential history相同,而且这些操作的先后顺序仍然得到保留.。那么这个history我们就称之为是linearizable的。
理解和区别Linearizability与Serializability,需要考虑到:很多操作并非是原子的(或者说是有延迟的)。即不能把操作或者事务或者事件看成一个瞬间完成的点,而是需要看成一个有开始和结束的线段。所以,线性一致性就是考虑的在不使用并发控制的情况下而达到的与串行化执行一致的结果。
需要注意的是Linearizability其实是无关于并发控制的, 它只是关于操作顺序的一种限制. 与Linearizability很容易混淆的一个术语是Serializability, 这个特性才是关于并发控制的一个限制. 如果一个系统, 可以将并发的事务按照某种调度, 达到的效果和某种串行执行的效果一样(也就是各事务的操作不会相互交错), 那么这个特性就叫Serializability.
具体来说,可线性化假设读写操作都需要执行一段时间,但是在这段时间内必然能找出一个时间点,对应操作真正“发生”的时刻。
其实上面的过程可以看出来,两者属于不同的维度概念,理想情况下的数据库应该满足 strict serializability,即隔离级别做到 serializable、一致性做到 linearizabile。
Linearizability场景举例
我们假设现在有一个寄存器, 具有W和R两个操作,W(0)表示写入0,W(1)表示写入1,R(x)同理。
- 场景1
这个并发执行的场景对应的history如下:
Start W(1) A
Start R(0) B
Commit W(1) A
Commit R(0) B
Start R(1) A
Commit R(1) B
这个history可以找到一个等价的sequential history,:
Start W(1) A
Commit W(1) A
Start R(0) B
Commit R(0) B
Start R(1) A
Commit R(1) B
所以,在这个场景下事务是Linearizability的,因为实际上和串行得到的结果是一致的。
- 场景2
Start W(0) A
Commit W(0) A
Start W(1) B
Start R(1) A
Commit R(1) A
Start W(0) C
Commit W(0) C
Commit W(1) B
Start R(1) B
Commit R(1) B
在这个场景中,我们看到W(1)在B写入的同时,R(0)也在被A读取;在W(0)完成提交之后,W(1)又提交了,所以在这之后的B读取到的是R(1)而非R(0)。
换言之,这实际上和串行执行的结果是不一致的。
Start W(0) A
Commit W(0) A
Start W(1) B
Commit W(1) B
Start R(1) A
Commit R(1) A
Start W(0) C
Commit W(0) C
Start R(0) B //不一致
Commit R(0) B //不一致
分布式系统中的Linearizability
分布式系统中, 如果可以按照操作发生的先后顺序,构造一个linearizable的运行记录,那么这个特性就称之为的Linearizability(线性)。
拿zookeeper来举例, 我们应该都知道zookeeper的follower read特性. 如果你从follower上读取数据, 你读到的数据可能是旧的(原因可能是新数据还没apply). 也就是t1时刻更新了zk节点, 在t1之后, 读到的数据可能还是t1之前的. 所以zookeeper是不满足线性的。
zookeeper不满足线性的原因是follower上的read操作和apply操作没有进行重排, 也就是后面的read操作先于t1那个事务的apply操作。
数据库中的Linearizability
现在的数据库系统, 都具有Snapshot read的概念, 所谓Snapshot read指的是对于系统的读操作, 只是过去某一时刻的一个快照, 也就是对应于该事务id所能看到的一份历史数据。
如果一个分布式系统能够对”历史”有所感知的话, 也就意味着能够感知操作的前后顺序, 也就是说这个系统支持Linearizability。
而如果一个分布式系统无法支持Linearizability, 那么对于指定一个timestamp所能看到的历史数据, 每次可能是不一样的。
举例来说, 假设现在有一个全球部署的分布式存储系统, 先后发生以下3个操作:
a. x = 1, 分配的timestamp为s1
b. 指定timestamp s2执行read x
c. x = 2, 分配的timestamp为s3
d. 指定同一个timestamp s2执行read x
假设s2 > tabs(a), 意思是s2是在操作a之后一个timestamp, 按照直觉来想, 肯定满足s2>s1的关系式, 但是对于不支持Linearizability的系统来说, 可能存在s2 < s1, 导致操作b这个Snapshot read无法看到a操作的结果。
线性一致性实践
如何保证线性一致性
解决上面zk或者数据库MVCC的问题,经典的解决方案是使用TT时间戳来解决:Distributed System Clocks分布式系统时钟解决方案。具体实例可以参考关于Spanner中的TrueTime和Linearizability一文,Spanner保证External Consistency(等价于 Linearizability),也就是说:
-
写
事务T1–>事务T2,能够保证T2的timestamp一定大于T1的timestamp。所以Spanner采用了TT(True Time API)的方式来保证这一特点,这是靠GPS和铯原子钟来实现相对准确的时间戳(TT的误差范围是1ms到7ms之间)。然后通过一个限制“Commit Wait”,也就是说为了消除误差范围,会等待至误差结束后才做提交。 -
读
在做读事务的时候,client会指定一个时间戳t
来做快照读,Spanner会找到一个数据充分更新好的replica来提供读服务,所谓充分更新好也是指消除时间戳误差后的正确replica。如果没有充分更新好,还需要等待误差消除。
timeslave以30s的间隔从time master同步时间. 当timeslave需要校准local clock时, 从就近的datacenter和较远的datacenter的time master拉取时间信息, 并通过Marzullo算法[4]识别并丢弃异常的时间来源, 并将多个时间信息归并到一个统一的时间。
一般等待误差消除的时间是平均误差时间*2。
注意,HLC/LC 并不满足线性一致性。即,因果一致性是弱于线性一致性的。为什么这么说,假如节点没有接收事件或者发送事件,那么这个系统就不是线性一致性的,可以参考下面的例子:
事务TA和TB 发生在不相交的节点 上,这种情况下,二者的(混合)逻辑时间戳是相互独立的,因此即使TB发起的快照读的绝对时间大于TA的时间戳,但是仍然会有TB发起的时间戳小于TA的时间戳的情况出现。
如何验证线性一致性
- WG算法:
请求的调用历史中,存在着一种偏序关系:Prev,如果一个请求的Return发生时间早于另一请求的Invoke,我们便称其Prev另一个请求。显而易见,这种偏序关系是一致性验证算法必须要保留的。祸兮福所倚,也正是这种对偏序关系的保留,给了算法加速的可能。
WG算法的思路非常简单:从调用历史中找出没有Prev的项,将其对应的请求执行并取出,之后对剩下的调用历史重复该算法,直到没有更多的调用历史或执行结果不满足。
之后还有对于WG算法的改进——WGL算法,通过缓存已经见过的配置,来减少重复的搜索。
- P-compositionality算法
P-compositionality算法利用了线性一致性的Locality原理,即如果一个调用历史的所有子历史都满足线性一致性,那么这个历史本身也满足线性一致性。因此,可以将一些不相关的历史划分开来,形成多个规模更小的子历史,转而验证这些子历史的线性一致性,例如kv数据结构中对不同key的操作。上面提到了算法的计算时间随着历史规模的增加急速膨胀,P-compositionality相当于用分治的办法来降低历史规模,这种方法在可以划分子问题的场景下会非常有用。