分布式领域最重要的一篇论文,到底讲了什么?
The following article is from 张铁蕾 Author 铁蕾
I realized that the essence of Johnson and Thomas’s algorithm was the use of timestamps to provide a total ordering of events that was consistent with the causal order.(译文:我意识到,Johnson和Thomas提出的算法的本质在于使用时间戳来提供事件的一种全局排序,而这种排序是和因果顺序保持一致的。)
再比如,这篇论文中定义的「Happened Before」关系,不仅在分布式系统设计中成为考虑不同事件之间关系的基础,而且在多线程编程模型中也是重要的概念。另外,还有让很多人忽视的一点是,利用分布式状态机来实现数据复制的通用方法(State Machine Replication,简称SMR),其实也是这篇论文首创的。
分布式系统中的事件和偏序关系
一个分布式系统由很多进程组成,而一个进程可以看成是一个事件序列。所以说,事件是一个抽象概念,根据应用场景不同,程序运行发生的任何事情都可以表示成事件。比如,根据论文中举的例子,一个子程序开始执行,可以看成是一个事件;一条机器指令的执行,也可以看成一个事件。
时间是一个物理学上的概念。每个事件发生的时候,都对应时间的某个数值。而根据相对论,时空是不可分割的,时间必须与空间一起讨论才有意义。
时钟分两种,一种是物理时钟(Physical Clock),或者叫实时时钟(Real Clock);另一种是逻辑时钟(Logical Clock)。物理时钟是对时间的一种度量;现实中的物理时钟肯定是有误差的。而逻辑时钟是跟物理时间无关的,用于对每一个发生的事件指派一个单调递增的数值,是系统执行节拍的一种内部表示。
两个不同的事件,可能具有先后关系,它们之间是能够排序的;也可能两个事件之间根本无法按照先后关系来排序。也就是说,事件排序是偏序的(Partial Ordering)。
同一进程内部先后发生的两个事件之间,具有「Happened Before」关系。比如,在进程Q内部,q2表示一个消息接收事件,q4表示另一个消息的发送事件,q2排在q4前面执行,所以q2→q4。
同一个消息的发送事件和接收事件,具有「Happened Before」关系。比如,p1和q2分别表示同一个消息的发送事件和接收事件,所以p1→q2;同理,q4→r3。
「Happened Before」满足传递关系。比如,由p1→q2,q2→q4和q4→r3,可以推出p1→r3。
Another way of viewing the definition is to say that a→b means that it is possible for event a to causally affect event b. Two events are concurrent if neither can causally affect the other.(译文:看待「Happened Before」关系的另一种方式,相当于是说,a→b意味着事件a有可能在因果性上对事件b产生影响。如果两个事件谁也无法影响对方,那么它们就属于并发关系。)
随着我们对于分布式系统设计经验的不断丰富,我们会发现这一概念将贯穿于分布式系统设计的任何细节中去。
逻辑时钟
考察进程Q内部的两个事件,q2→q4,而C〈q2〉 = 52 < C〈q4〉 = 54。
再考察同一个消息的发送事件和接收事件,p1→q2,而C〈p1〉 = 40 < C〈q2〉 = 52。
为什么又需要全局排序?
但是,我们发现C〈p3〉 = 53,C〈q4〉 = 54,而53 < 54,所以人为指定一个次序,即认为p3是在q4之前发生的。实际上,由于这两个事件之间不存在「Happened Before」关系,我们不管是认为p3在q4之前发生,还是认为q4在p3之前发生,都没有大碍。
逻辑时钟的异常行为
一方面,这是因为逻辑时钟的定义特意避开了物理时间,系统产生的时间戳与请求的真实时间先后并没有直接关系;另一方面,在系统内部,请求A与请求B这两个事件之间,并不存在「Happened Before」关系,因此并不保证请求B的时间戳一定比请求A的时间戳更大。结果,请求B由于被打上了一个更小的时间戳,有可能在全局排序中排到了请求A的前面,所以Bob并没有查询到最新的球赛比分(也许只查询到了旧版本的数据,这里的行为取决于一些额外的数据库实现机制),因此与Alice看到的结果产生了不一致。
那么,Alice通知Bob的速度,最快可以达到什么程度呢?答案是:光速。因为在这个世界上信息传递的最快速度就是光速了。现在我们假设,如果系统的物理时钟满足这样一个最苛刻的条件:即使Alice通知Bob的速度达到光速,系统也总是能保证对请求B打上的时间戳要大于请求A的时间戳,那么,就能保证永远不出现前面的异常行为。这样的一个时钟条件,在Lamport的论文中被称为Strong Clock Condition。具体描述如下:
“→”表示「Happened Before」关系;
而“➜”表示由相对论定义的事件偏序关系。
时空本身定义了一种偏序
一个事件可能对它的未来光锥内部的任何事件产生因果性上的影响。
一个事件与它的未来光锥内部的任何事件之间,满足一个偏序关系,即“➜”。
物理时钟同步算法
时钟的运行速率跟真实时间的流逝速率可能有差异;
任意两个时钟的运行速率有差异,它们的读数会漂移得越来越远。
对于进程内发生的不同事件,必须保证后发生的事件比先发生的事件时间戳要大。这实际上是要求我们保证每个物理时钟实例的读数总是单调递增的。这是比较容易实现的,我们只需要在微调时钟读数的时候,只把读数调大而不把读数调小。
对于发生在两个不同进程上的事件,就比较麻烦了。我们还是再考察一下前面Alice和Bob的例子。Bob之所以感受到了异常,是因为Alice通过系统外的一种方式通知了Bob,于是在请求A和请求B这两个事件之间确立起了偏序关系。现在为了保证不出现异常行为,我们就要求,不管Alice向Bob传递信息这个过程发生的速度有多快(最快可以达到光速,但在实际系统中,由于网络延迟和缓存等原因会慢很多),请求B发生时的时钟读数都必须大于请求A发生时的时钟读数。这个要求可能没法被满足的原因,在于两边的物理时钟可能有误差。
因此,就需要在不同的物理时钟之间交换信息,并借助这些信息同步时钟读数。通过这种方式,预期可以把时钟之间的误差控制在一定范围内。可以这么说,只要我们在时钟之间交换信息足够频繁,就有希望做到这一点。
这是两种机制的赛跑:一方面,Alice通过系统外的方式向Bob传递信息,只要这个过程足够快,他们就有可能“看到”时钟误差造成的时钟读数减退(也就是出现了异常行为);另一方面,物理时钟同步算法通过在时钟之间不断交换信息并按照一定规则调整时钟读数,将时钟误差控制在一定范围内。只要算法的各个参数设置得当,就能保证:即使Alice向Bob传递信息的速度达到物理极限——光速,他们也无法“看到”时钟读数的减退现象。于是,Strong Clock Condition就被满足了。