引言
随着信息科技的在人类生活中的不断渗透, 我们数据规模和计算规模也是与日俱增, 某些问题已经无法在单机系统上进行处理, 只能寻求分布式系统的解决方案.
分布式系统用多个设备来处理特定问题, 但是, 如何保证这样的处理结果像单机系统那样正确?
这里所谓的正确的指的是: 以相同的顺序进行相同的一系列操作, 分布式系统能得到和单机系统相同的结果.
分布式系统中有个概念叫做Linearizability(线性), 可以用来衡量一个分布式系统的正确程度. 也就是本文主要介绍的内容.
本文主要介绍Linearizability概念, 实现Linearizability的意义, 实现Linearizability的难点. 以及简单说明它与Serializability的区别.
Linearizability的概念
如果一系列的操作并发交叉进行, 最终形成的history(可以理解为运行记录), 与顺序执行这些操作形成的sequential history相同, 而且这些操作的先后顺序仍然得到保留. 那么这个history我们就称之为是linearizable的[1].
这个说法是来自论文[1]的正式定义, 理解起来有点烧脑. 但是我有个直白的理解:
先是简单的情况, 假设操作都可以瞬间生效, 那write操作写入一个值之后, 后续的read操作都应该能读到这个值, 或者读到更加后面的write的值. 而且一个read操作读到某个值后, 它后面的read应该也读到这个值, 或者更加后面的write的值. 这样的一系列操作就是满足linearizable的.
复杂的情况来了(偏学术), 现实场景中, 一个操作从开始到结束普遍都是有时延的. 比如下文场景一中, 操作1和2的运行时间有一部分重叠, 我们无法区分两者的顺序, 但是可以明确的是: 3在1结束之后开始运行, 而且操作3也在2之后运行. 所以操作3必然能读到操作1的值, 或者读到操作2的值. 这样的话, 这一些列操作就是满足linearizable的.
现在我列举两个场景, 来形象说明这种偏学术的复杂情况.
我们假设现在有一个寄存器, 具有W和R两个操作, 比如W(0)A表示进程A往寄存器写入0这个值, R(0)B表示进程B从寄存器读到0这个值.
现实中, 操作从开始到结束是具有时延的, 我们用start[W(0)]表示发起操作, end[W(0)]表示请求结束.
场景一
假设现在有如下图所示的场景一:
h1
该场景共有三个操作:
A进程执行W(1).
操作1进行的过程中, B读取到0这个值, 用R(0)表示.
操作1和操作2都结束后, B进程再次读取, 得到了1这个值, 用R(1)表示.
注意这三个操作中隐含的先后顺序: 操作1先于操作3, 操作2先于操作3.
这个并发执行的场景对应的history如下:
这个history可以找到一个等价的sequential history,:
而且这个sequential history相当于依次执行: 操作2, 操作1, 操作3.
这个sequential history仍然保持了操作1--->操作3, 操作2--->操作3的先后顺序. 也即是等价于场景一.
因此, 根据论文[1], 场景一可以认为是linearizable的.
场景二
再看如下的场景二, 该场景并不满足linearizable的限制 :
h2
在这个场景中, 当B执行W(1)的同时, A也在读取, 而且已经读取到了1这个值, 也即是有以下关系成立:
之后C执行W(0), 也即是:
W(0)C结束之后, B读取这个值, 也就是有以下关系成立:
从中, 我们发现B竟然还读取到1这个值, 它其实是应该读到0这个值的.
所以这个场景是不满足linearizable.
分布式系统中的Linearizability
分布式系统中, 如果可以按照操作发生的先后顺序, 构造一个linearizable的运行记录, 那么这个特性就称之为的Linearizability(线性).
拿zookeeper来举例, 我们应该都知道zookeeper的follower read特性. 如果你从follower上读取数据, 你读到的数据可能是旧的(原因可能是新数据还没apply). 也就是t1时刻更新了zk节点, 在t1之后, 读到的数据可能还是t1之前的. 所以zookeeper是不满足线性的.
zookeeper不满足线性的原因是follower上的read操作和apply操作没有进行重排, 也就是后面的read操作先于t1那个事务的apply操作.
解决这个问题的办法, 可以参考tidb的做法, 将read操作也进行一次raft log, 相当于对read操作的顺序重排到了raftlog中, 排在apply之后.
不满足线性的另一种原因是, 无法感知/重排这些操作的先后顺序. 举例来说, 假设现在有一个全球部署的分布式存储系统,先后发生以下4个操作
按照直觉来想, 操作b读取到的x值肯定是1, 操作d读取到的x值肯定是2. 但这一切都是建立在系统能够感知这些操作的先后顺序之上, 比如在操作b中, 坐落于美国的server可以意识到操作a已经先发生.
但是在现实的分布式场景中, 我们的系统并不能像上帝一样, 可以清楚的知道世界各角落中, 每个操作的先后顺序, 除非我们为每一个操作都标记上统一的, 单调递增的id, 这个id的功能一般由timestamp来充当.
但是, 对于Spanner[2][3]这样全球部署, 或者跨地域部署的系统, 如何来为事务分配timestamp, 才能保证系统的响应时间在可接受的范围内? 如果整个系统采用一个中心节点来分配timestamp, 那么系统的响应时间就变得非常不可控, 对于离中心节点隔了半圈地球的用户来说, 响应时间估计会是100ms级别.
所以, 我有一篇文章着重介绍Spanner分配timestamp的原理, 也即是TrueTime API.
Linearizability与Serializability
需要注意的是Linearizability其实是无关于并发控制的, 它只是关于操作顺序的一种限制. 与Linearizability很容易混淆的一个术语是Serializability, 这个特性才是关于并发控制的一个限制. 如果一个系统, 可以将并发的事务按照某种调度, 达到的效果和某种串行执行的效果一样(也就是各事务的操作不会相互交错), 那么这个特性就叫Serializability.
Serializability着重于保证编程模型中的一些约束, 这些约束大部分是人为规定的. MySQL的默认RR隔离级别是没有保证Serializability, 因为RR并不能防止Lost Update和Write Skew的发生, 这两种情况都会破坏一些约束, 具体请参考Weak Isolation in Relational Databases[4].
对于单机数据库系统, 单机数据库系统维持一个统一的, 递增的事务id是轻而易举的事情, 所以可以认为它们天生就是具有Linearizability特性. 另外, 有些数据库系统, 比如MySQL, 它所支持的Serializable隔离级别便是本文所说的Serializability的特性.
所以, 我们可以看到, 所谓的Serializability便是ACID中的I, 至于Linearizability, 是针对于分布式系统而言的一个难点.
Linearizability的意义
现在的数据库系统, 都具有Snapshot read的概念, 所谓Snapshot read指的是对于系统的读操作, 只是过去某一时刻的一个快照, 也就是对应于该事务id所能看到的一份历史数据, 在Spanner的场景来说, 也就一个指定的timestamp所能看到的一份历史数据.
如果一个分布式系统能够对"历史"有所感知的话, 也就意味着能够感知操作的前后顺序, 也就是说这个系统支持Linearizability.
如果一个分布式系统无法支持Linearizability, 那么对于指定一个timestamp所能看到的历史数据, 每次可能是不一样的, 举例来说, 假设现在有一个全球部署的分布式存储系统,先后发生以下3个操作:
假设s2 > tabs(a), 意思是s2是在操作a之后一个timestamp, 按照直觉来想, 肯定满足s2>s1的关系式, 但是对于不支持Linearizability的系统来说, 可能存在s2
类似, 还假设s2 s3, 导致操作d这个Snapshot read看到了c操作的结果.
注意, 操作b和d的都指定同一个timestamp=s2进行Snapshot read, 但是b和d看到的历史数据却是不一样的, 也就是说, 对于同一个timestamp对应的快照竟然不一样.
到这里可以看出来, 不支持Linearizability的分布式系统, Snapshot read便无从谈起, Snapshot Isolation也就是不可能的事情.
有些系统实现了"部分顺序", 也就是在单个节点(或者说单个分区)内的Linearizability, 也就是能做到单机Snapshot, 但是没有全局的Snapshot.
实现Linearizability的难点
从上述描述中, 可以看到, 实现Linearizability首先需要对系统中每个事务分配一个统一的,单调递增的id, 一般来说是timestamp. 那么, 在分布式系统中, 如何协调这个timestamp呢?
对于这个问题, 我们可能会很迅速的想到一个方案: 引入一个专门生成timestamp的中心节点, 每次事务提交时, 访问该中心节点来获得timestamp. TiDB使用的便是这种方案, 它利用TimeStamp Oracle模块来提供授时服务. 但是这种方案引入了中心节点, 也意味着整套系统的部署不可能在地理位置上过于分散, 否则事务延时将会令人难以忍受.
需要指出的是, 逻辑时钟(比如Lamport时钟和Vector时钟)无法实现Linearizability, 因为它只能区分有"因果关系(也就是有通信)"的事件间的顺序. 所以, 类似于"事务T2在事务T1提交后才开始提交"这样的场景, 当节点间没有发生通信时, 逻辑时钟无法区分这两个事务的顺序.
那么, Spanner是如何在这种全球部署的分布式系统中保证Linearizability的. 在全球分布式系统中, 真实世界里两个事务, 先后在不同的地方commit, 系统怎样去如实反映事务的commit顺序? 具体详情可以参考这篇文章:关于Spanner中的TrueTime和Linearizability
参考
领取专属 10元无门槛券
私享最新 技术干货