Time, Clocks, and the Ordering of Events in a Distributed System (1978)
简介
本文是分布式系统理论的开山鼻祖、2013 年图灵奖获得者 Lamport 的成名作,也是分布式计算领域杰出论文最佳影响力奖 Dijkstra Prize 的第一篇论文,高达 11692 的引用量(截至 2019/12/08)足以证明其广泛的影响力:
本文主要讨论 3 个话题:
- 分布式系统中的事件偏序
- 利用逻辑时钟实现事件偏序
- 利用逻辑时钟实现事件全序
事件顺序
生活中的事件顺序
生活中,当两个事件 A 和 B 发生时,我们可以利用其发生的时刻来确定它们的先后关系,如:
A:2019-12-08T00:00:00+00:00
B:2019-12-07T08:00:00+00:00
这样就能很容易地确定 B 在 A 之前发生。这是因为:
- 这些事件发生时刻的度量粒度比较大,从分钟、小时到日、月、年
- 记录这些事件发生的时间戳通常精确到秒
只要二者时间戳不完全相等,我们就能准确判断先后顺序。
计算机中的事件顺序
在计算机中,同一秒内可能发生大量的事件:小到一个 CPU 指令,大到一段 shell 脚本的执行。我们是否需要确定计算机中所有事件的绝对顺序,才能保证计算过程的正确性?显然并非如此,以这一段代码为例:
1 | func main() { |
我们希望计算机输出:
1 | Hello |
那么不论这台计算机上发生什么事情,只要 fmt.Println("Hello")
发生在 fmt.Println("World")
之前,这段程序就是正确的。根据冯诺依曼计算机的工作原理,PC 寄存器会先指向 fmt.Println("Hello")
的指令地址,执行完以后再移向 fmt.Println("World")
,所以二者的先后顺序能够被保证。
再看下面这段代码:
1 | func main() { |
这时两个打印语句分别位于不同的线程中,二者的执行顺序无法保证。但通常按这种方式构建的程序是为了充分利用系统的 CPU,其正确性本身不受二者的执行顺序影响。即这是程序员有意识地在用顺序换时间。
分布式系统
分布式系统由位于相同或不同空间上的进程/线程集合构成,进程/线程之间通过收发信息来通信。如果一个系统中消息传递延迟相对于事件间隔不可忽略,就可称之为分布式系统。在分布式系统中,判断事件的先后顺序并非易事,以下图为例:
P1、P2 分别表示某系统中两个不同的进程/线程,图中的圆圈代表其上发生的事件。
假设消息传递延迟相对于事件间隔可以忽略,那么
假设消息传递延迟相对于事件间隔不可忽略,如下图所示:
如果我们对
- 对于
:可以确定的顺序为 ,但 、 与 之间的关系无法确定 - 对于
:三个事件任意的全排列皆有可能
如果我们假设二者通过 TCP 传输,那么
偏序
对于分布式系统,我们可以定义 "
- 如果
和 是同一个进程/线程中的两个事件,如果 在 之前发生,那么 - 如果
事件是 " 发送消息 到 ", 事件是 " 收到 发送的消息 ",那么 - 如果
且 ,那么 - 如果
且 ,那么 和 是并发 (concurrent) 事件
如果再加上一条假设
其中可以确定的先后关系是
逻辑时钟
为了能显式地表示分布式系统中事件的偏序,我们需要引入逻辑时钟,顾名思义,这种时钟与物理时钟没有任何关系。假设每个进程
根据上文中偏序的讨论,一个正确的逻辑时钟需要满足:
Clock Condition:对任意两个事件 a 和 b,如果
,那么
需要注意的是,Clock Condition 的逆命题 "如果
如果以下两个条件成立:
- C1:如果
和 是进程 中的两个事件,且 a 先于 b 发生,那么 - C2:如果进程
向进程 发送消息,a 表示发送事件,b 表示接受事件,那么
那么我们可以说服自己 "Clock Condition 成立"。顺着思路,我们可以构建这样一个逻辑时钟:
- IR1:在进程内部,每发生一个事件,逻辑时钟绝对值自增一次
- IR2:
- 如果
事件是 " 发送消息 到 ",那么 需要包含 发送消息时的逻辑时间戳 - 当
接收到消息 时,必须将自身的逻辑时钟与 对齐,即
- 如果
只要按照 IR1 和 IR2 构建逻辑时钟,逻辑时钟
记住这个逻辑时钟的实现,你会在不同的共识算法中看见它的身影
全序
我们已经有了事件的偏序,有没可能基于此构建分布式系统事件的全序?回顾下面这个图:
只要能够确定
只要有一种方式能从中稳定的选一种,问题就解决了。
其实这个问题的解决方案出奇地简单,我们只要能给系统中的所有进程排个序即可,假设
下面给出严谨的定义:假设进程间的顺序关系用 "
且
场景举例
实现事件的全序是保障分布式系统正确性的起点,这意味着系统中的多个节点能够达成共识。
想象这样一个具体场景:一组进程需要排他地使用某个资源,如何能保证所有进程以没有分歧的方式轮流使用这个资源?即满足:
- 当前进程释放资源后,其它进程才能使用资源
- 资源必须按照请求的先后顺序来赋予其它进程
- 只要每个进程在使用完资源后会遵守规定释放资源,那么每个请求最终都将被满足
调度中心方案
我们能否用唯一的一个调度进程
但想象这样一个场景:
先向 发送资源使用请求,然后向 发送一条消息 收到消息后向 发送资源使用请求 - 因为网络传输的延迟问题,
的请求先于 的请求到达 将资源先分配给 ,等 释放资源后再分配给
但是根据事件的偏序关系,我们知道
全序方案
基于我们构建全序的方案,我们来尝试解决这个问题。为了简化逻辑,我们先制定一个假设:
对于任意两个进程
和 ,从 发送到 的消息将按顺序到达
即使没有该假设,我们仍然可以自行通过类似 TCP 的 sequence number 和 ack 机制使该假设成立。
每个进程
- 请求资源时,进程
需要向所有其它进程 发送消息 " : 请求使用资源",消息中包含 的当前时间戳,同时把请求也放进本地的请求队列中 - 当进程
收到请求后,将请求放入请求队列后,返回 ack 给 ,ack 消息中包含 当前的最大时间戳 - 当进程
要释放资源时,从本地的请求队列中移除自己的请求 " : 请求使用资源",并向其它进程 发送释放消息 " : 释放资源" - 当进程
收到释放请求时,从本地请求队列中移除相应的请求 " : 请求使用资源" - 当满足以下条件时,进程
被赋予资源的访问权: - 按照逻辑时钟的顺序,消息 "
: 请求使用资源" 发生的时间 最早 已经收到过其余所有进程的时间戳大于 的消息
- 按照逻辑时钟的顺序,消息 "
证明
论文上有相关的证明推理过程,这里不再复述。这里想说的是:确定全序的核心在于 "保证每个进程在做出占用或释放资源的决定之前已经充分了解到其它进程的信息"。
根据第 5 点:在
我利用这个 项目 验证了这个方案,有兴趣不妨一看。
方案对比
如果场景的要求是:"全局上看,所有进程按照一个确定的顺序使用和释放资源",那么 调度中心方案 是满足要求的,这里要求 "资源必须按照请求的先后顺序来赋予其它进程",主要在于我们要求整个系统中的所有事件存在全序关系。值得关注的是,这里的 全序方案 不需要 master 节点,所有节点的身份相同,你可以将它与如今流行的分布式共识算法的选主阶段关联起来理解。
外部事件
在上文的讨论中,实际上隐含着一个假设:
所有的事件都发生在系统内部
如果系统内部的两个事件
举例如下:假设
物理时钟
NOTE: 如果不是特别感兴趣,本节可以跳过
要解决外部事件引起的问题,就必须引入物理时钟。假设
PC1:存在很小的常数
,使得对于任意 i,有 成立
通常石英钟的
单个时钟的稳定还不够,不同的时钟应该尽可能同步,即:
PC2:存在很小的常数
对于任意 i, j,有 成立
设
可以推导出:
即,只要上述表达式成立,那么外部事件也将不会影响系统的正确性。从该表达式也可以看出:进程之间距离越近,即
容忍外部事件的全序
接下来,我们将提出一个分布式系统事件全序算法的改进版本,它将保证 PC2 成立(PC1 通过物理时钟本身决定),从而在分布式系统中确定容忍外部事件的全序。
假设
- IR1': 对于任意未接收消息的时刻
,进程 的物理时钟 可导,且满足 - IR2':
- 如果
在 时刻发送消息 ,那么 须包含当前时间戳 - 当
在时刻 收到消息 后,将自身的物理时钟调整为
- 如果
看起来和原始的全序算法十分相似,具体证明可自行翻阅论文附录。
小结
论文讨论了分布式系统中的事件偏序,并通过一个逻辑时钟实现这样的偏序,然后进一步为分布式系统的事件赋予全序。当存在外部事件影响系统内部事件的因果关系时,逻辑时钟无能为力,这时理论上我们可以通过引入满足一定要求的物理时钟来解决这个问题。
但本文讨论的话题是建立在系统运行顺利的基础上,提出的算法不具备容错能力。但它是我们进一步理解分布式系统理论的基石。