引言
随着信息科技的在人类生活中的不断渗透,数据规模和计算规模也与日俱增,某些问题已经无法在单机系统上进行处理,只能寻求分布式系统的解决方案。
分布式系统用多个设备来处理特定问题,但是如何保证这样的处理结果像单机系统那样正确?这里的正确指的是:以相同的顺序进行相同的一系列操作,分布式系统能得到和单机系统相同的结果。
概要
本文主要介绍了分布式系统中 Linearizability 概念,以及它与 Serializability 的区别,并介绍了 Spanner 中是如何通过 TrueTime 来实现 Linearizability。
Linearizability 的概念
如果一系列的操作并发交叉进行,最终形成的 history(可以理解为运行记录)与顺序执行这些操作形成的 sequential history 相同,而且这些操作的先后顺序仍然得到保留,那么这个 history 我们就称之为是 Linearizability 的 [1]。
我们假设现在有一个寄存器,具有 W 和 R 两个操作,比如 W(0)A 表示进程 A 往寄存器写入 0 这个值,R(0)B 表示进程 B 从寄存器读到 0 这个值。
现实中,操作从开始到结束是具有时延的,我们用 start[W(0)] 表示发起操作,end[W(0)] 表示请求结束。
假设现在有如下图所示的场景一:
该场景共有三个操作:
A 进程执行 W(1);
操作 1 进行的过程中, B 读取到 0 这个值, 用 R(0) 表示;
操作 1 和操作 2 都结束后, B 进程再次读取, 得到了 1 这个值, 用 R(1) 表示。
注意这三个操作中隐含的先后顺序:操作 1 先于操作 3,操作 2 先于操作 3。
这个并发执行的场景对应的 history 如下:
start[W(1)]A
start[R(0)]B
end[W(1)]A
end[R(0)]B
start[R(1)]B
end[R(1)]B
这个 history 可以找到一个等价的 sequential history:
start[R(0)]B
end[R(0)]B
start[W(1)]A
end[W(1)]A
start[R(1)]B
end[R(1)]B
而且这个 sequential history 中仍然保持了操作 1 与操作 3,操作 2 与操作 3 的先后顺序。因此场景一可以认为是 Linearizability 的。
再看如下的场景二,该场景并不满足 linearizable 的限制:
在这个场景中,当 B 执行 W(1) 的同时,A 已经读取到了 1 这个值,之后 C 执行 W(0),再之后 B 读取这个值,竟然还读取到 1 这个值。
分布式系统中,如果可以按照操作发生的先后顺序,构造一个 Linearizability 的运行记录,那么这个特性就称之为的 Linearizability。
举例来说,假设现在有一个全球部署的分布式存储系统,先后发生以下 4 个操作:
a. x = 1, 在中国发生
b. read x, 在美国发生
c. x = 2, 在日本发生
d. read x, 在美国发生
按照直觉来想, 操作 b 读取到的 x 值肯定是 1, 操作 d 读取到的 x 值肯定是 2。但这一切都是建立在系统能够感知这些操作的先后顺序之上, 比如在操作 b 中, 坐落于美国的 server 可以意识到操作 a 已经先发生。
但是在现实的分布式场景中, 我们的系统并不能像上帝一样, 可以清楚的知道世界各角落中, 每个操作的先后顺序, 除非我们为每一个操作都标记上统一的单调递增的 id, 这个 id 的功能一般由 timestamp 来充当。
但是对于 Spanner[2] 这样全球部署或者跨地域部署的系统, 如何来为事务分配 timestamp, 才能保证系统的响应时间在可接受的范围内? 如果整个系统采用一个中心节点来分配 timestamp, 那么系统的响应时间就变得非常不可控, 对于离中心节点隔了半圈地球的用户来说, 响应时间估计会是 100ms 级别。
所以, 本文会着重介绍 Spanner 分配 timestamp 的原理, 也即是 TrueTime API。
Linearizability 与 Serializability
需要注意的是 Linearizability 其实