条分缕析分布式:因果一致性和相对论时空
为什么要考虑因果一致性?
针对可能出现的数据不一致情况实施补偿措施 (compensation)。这需要在分布式系统之上的应用层面进行额外的处理,是非常容易出错且费时费力的。
基于CALM定理和CRDTs,完全消除补偿操作。但这样做其实限制了应用编程能够使用的操作类型,也就限制了系统能力。
不存在比因果一致性更强的一致性模型,能够在网络分区的情况下仍然可用。
在一个永远保持可用且单程收敛 (always-available, one-way convergent) 的系统里,因果一致性是可以被实现出来的。
因果一致性的直观解释
有一天,Billy失踪了。Sally找不到他的儿子,很着急,于是在社交网站上发布了一条状态:“我儿子Billy丢了!”
过了一会,Sally突然接到了儿子的电话。Billy告诉妈妈,他跑到朋友家玩去了。Sally长出一口气,又重新修改了刚才发布的状态:“谢天谢地,虚惊一场!Billy原来是跑出去玩了。”
Sally的一个朋友,叫James,看到了她发的最新的状态,在社交网站上回复了她:“太好了,总算松了一口气!”
Sally说:“我儿子Billy丢了!”
James回复:“太好了,总算松了一口气!”
单进程写操作有序。
“writes follow reads”规则。
因果关系可传递。
单进程写操作有序,指的是一个进程的多个写操作(可能是针对不同的数据对象的),在所有进程看来都是遵循同样的执行顺序。对应到前面的例子中,Sally的所有朋友都会看到,Sally是先发了一条状态“我儿子Billy丢了!”,然后又发了第二条状态“谢天谢地,虚惊一场!Billy原来是跑出去玩了。”这条规则保证了没有人是先看到第二条状态然后才看到第一条状态的。对于这个次序的保证也是因果性的一种体现。
“writes follow reads”指的是,假设第一个进程先读取到了数据对象x=5,后写入了另一个数据对象y=10,然后第二个进程读到了y=10,那么接下来如果这个进程读取数据对象x的值,那么不能读到一个比x=5更旧的值。这条规则在不同进程的不同数据对象之间建立了因果关联。对应到前面的例子中,James看到了Sally发的最新状态:“谢天谢地,虚惊一场!Billy原来是跑出去玩了。”(相当于读到了x=5),然后回复说:“太好了,总算松了一口气!”(相当于写入了y=10),再然后Henry看到了James的回复内容(相当于第二个进程读到了y=10),这时候站在Henry的视角上看Sally发布的状态,他不能比James看到的数据版本更旧。前面的例子中出现的问题就在于,Henry比James看到了更旧的一个数据版本:“我儿子Billy丢了!”,致使因果关系混乱了。
因果关系是具有传递性的。如果操作A和操作B在因果关系上满足先后次序,而且操作B和操作C在因果关系上也满足先后次序,那么A和C在因果关系上必然是满足先后次序的。
因果一致性的精确定义
A --> wi(x),表示一个写操作:第i个进程向数据对象x写入了值A。
ri(x) --> A,表示一个读操作:第i个进程从数据对象x中读到了值A。
(1) o1和o2属于同一个进程,且o1在o2前面执行。
(2) o1是个写操作,o2是个读操作,且o2读到的值是由o1写入的。
(3) 存在一个操作o'满足o1→o'→o2。
(1) 同一个进程内部先后执行的两个操作,不管他们是读操作还是写操作,都是满足因果顺序的。比如上图中P1进程的 A -->w1(x) 和 B --> w1(x) 两个操作就是满足因果顺序的;P2进程的r2(x) --> B 和 C --> w2(y) 也是满足因果顺序的。这一条件表明,因果顺序遵从了进程的执行顺序。
(2) 如果一个读操作读到的值是由另一个写操作写入的(肯定是针对同一个数据对象),那么不管它们是不是属于同一个进程,这个写操作和读操作就是满足因果关系的。比如上图中的 B -->w1(x) 和 r2(x) --> B 就是满足因果顺序的;C --> w2(y) 和 r3(y) --> C 也是满足因果顺序的。这个条件反映了读写操作之间的因果依赖关系。
(3) 这个条件表明因果顺序“→”满足传递关系 (transitive relation)。
因果顺序是一种偏序关系。所谓偏序关系,就是说只有一部分操作是可以按照因果顺序进行比较的,而有一些操作之间是不能比较的。比如上图中的 A --> w1(x) 和 D --> w3(x) 这两个操作之间的关系,就不符合因果顺序三个条件中的任何一个。r2(x) --> B 和 D --> w3(x) 之间也同样如此,它们之间不存在因果顺序。这具有很深刻的物理学和哲学内涵,因为现实世界的事件之间的因果关系就是偏序的(先不展开讨论)。分布式系统里对于读写操作之间的这种因果顺序的定义,正是对现实世界中这种现象的一种刻画。
因果顺序不能有循环依赖。假如操作o1→o2→o'→...→o1,由传递关系就应该有:o1→o1。这表示一个操作是它自己的「因」,这是荒谬的。换句话说,如果把因果顺序用一个图来表示,那么它应该是一个有向无环图 (directed acyclic graph)。
条件I:重排后的序列中每一个读操作返回的值,必须等于前面对同一个数据对象的最近一次写操作所写入的值。
条件II:重排后的序列遵从前面定义的因果顺序“→”。
顺序一致性是对所有进程的所有读写操作进行统一的重排,而因果一致性是站在每个进程的视角各自进行局部重排。这表示顺序一致性要求系统的所有进程都对操作排序达成一致的看法,而因果一致性允许每个进程对操作的排序有不同的看法。
因果一致性与顺序一致性的条件II不同。顺序一致性的条件II只是要求遵从进程的执行顺序,而因果一致性则有更强的要求——遵从因果顺序(而进程的执行顺序只是因果顺序的一部分)。
D --> w3(x)
A --> w1(x)
B --> w1(x)
C --> w2(y)
D --> w3(x)
A --> w1(x)
B --> w1(x)
r2(x) --> B
C --> w2(y)
D --> w3(x)
A --> w1(x)
B --> w1(x)
C --> w2(y)
r3(y) --> C
r3(x) --> B
为什么因果一致性是站在各个进程的视角对部分操作进行排序,而不是对所有进程的操作进行全局排序?这是因为,因果顺序是一种偏序关系,这就允许站在不同进程的视角去观察各自所关心的部分操作,从而得到不同的观察结果(排序序列)且同时不违反因果律。假如因果顺序不是一种偏序,而是一种全局关系,那么就可以把所有操作按照同一个次序排序起来,那就变成跟顺序一致性一样了,每个进程也可以看到完全一样的排序序列了。所以说,这里隐含着一个结论:因果一致性是比顺序一致性更弱的一类一致性模型,而顺序一致性也意味着遵从了因果一致性。另外,也只有当站在不同进程的视角有不同的观察结果时,才可能在发生网络分区的时候,同时提供可用性。想象当一个节点同系统其它部分隔开了,这个节点不需要等待与其它节点联系,仍然可以使用旧版本的数据提供服务,同时不违反因果顺序即可。而如果像顺序一致性或者线性一致性那样,维持一个统一的全局排序,则需要在各个节点之间充分交换完数据才能达成一致。
为什么站在一个进程的视角要考虑所有其它进程的写操作呢?因为对于因果顺序来说,所有写操作都是潜在的「因」,而当前进程的读操作则代表了它的「看法」。进程的局部看法的形成,需要考虑所有的「因」,才能保证不违反因果律。
单进程写操作有序。因为每个进程进行局部重排的时候,都把所有进程的写操作考虑进去了,所以任何一个进程的多个写操作,在所有进程看来都是遵循同样的执行顺序。比如前面例子中P1的两个操作 A --> w1(x) 和 B --> w1(x),在三次重排序列中都能保持次序。
“writes follow reads”规则。隐含在因果顺序的前两个条件里面。在前面例子中,进程P2先是读取到了x=B,后写入了y=C,然后进程P3先是读到了y=C,接下来进程P3读取x的值,不能读到一个比x=B更旧的值(满足)。
因果关系可传递。隐含在因果顺序的第三个条件里。
分布式系统事件排序
在进程Q内部,q2表示一个消息接收事件,q4表示另一个消息发送事件,q2排在q4前面执行,所以q2→q4。
p1和q2分别表示同一个消息的发送事件和接收事件,所以p1→q2;同理,q4→r3。
“happened before”满足传递关系。由p1→q2,q2→q4和q4→r3,可以推出p1→r3。
因果一致性的难以理解之处
A --> w1(x)
B --> w2(x)
r1(x) --> B
B --> w2(x)
A --> w1(x)
r2(x) --> A
在P1看来,A --> w1(x)发生在B --> w2(x)前面。
在P2看来,B --> w2(x)发生在A --> w1(x)前面。
更进一步
一个事件,和在它的「未来光锥」之内的任何其它事件,都是有偏序关系的。它们之间的先后次序,不管在什么参照系上看到的都应该是相同的。
一个事件,和在它的「未来光锥」之外的其它事件,是没有偏序关系的(也就不可能产生因果关系)。站在不同参照系上看这样的两个事件,它们的先后次序可能不同。
参考文献:
[1] Martin Kleppmann,《Designing Data-Intensive Applications》, 2017.
[2] Martin Kleppmann, "Please Stop Calling Databases CP or AP", 2015.
[3] Peter Bailis, Ali Ghodsi, "Eventual Consistency Today: Limitations, Extensions, and Beyond", 2013.
[4] Werner Vogels, "Eventually Consistent", 2008.
[5] Prince Mahajan, Lorenzo Alvisi, Mike Dahlin, "Consistency, Availability, and Convergence", 2011.
[6] Peter Bailis, Ali Ghodsi, et al, "Bolt-on Causal Consistency", 2013.
[7] Mustaque Ahamad, Gil Neiger, James E. Burns, et al, "Causal Memory: Definitions, Implementation and Programming", 1994.
[8] Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System", 1978.
其它精选文章:
Redis为什么用跳表而不用平衡树?
漫谈分布式系统、拜占庭将军问题与区块链
看得见的机器学习:零基础看懂神经网络
基于Redis的分布式锁到底安全吗
深度学习、信息论与统计学
蓄力十年,做一个成就
三个字节的历险
由「精益创业」所想到的
知识的三个层次
扫码或长按关注微信公众号:张铁蕾。
有时候写点技术干货,有时候写点有趣的文章。
这个公众号有点科幻。