此文是分布式系统领域的经典论文,定义了分布式系统中事件的偏序关系,并且提出了一种分布式系统的时间同步算法。
在多进程通信时,独立的计算机也可能是分布式系统,换句话说,只要不同进程之间的通信时延不能忽略都可以算作分布式系统。 在分布式系统的运行过程中,事件的执行顺序非常重要,论文首先明确定义了事件的 happened before 关系, 进而阐明了事件的偏序关系(不叫全序的原因是有些事件不能以物理时钟的视角进行排序,可以看做是并行的,没有绝对的先后顺序), 为了实现对分布式系统中所有事件排序,又定义了全序关系,在偏序关系的基础上用进程的逻辑时钟对没有先后顺序的事件进行排序。
happened before(a $→$ b): 偏向对事件的逻辑关系进行排序,事件 a 会对事件 b 造成潜在的影响,称 a happened before b。
- 对于单进程,只要 a 的执行顺序是在 b 之前,即为 a $→$ b。
- 对于多个进程,一个进程发送事件(a)给另一个进程,并且另一个进程收到这个事件(b),也算是 a $→$ b。
- happened before 有传递性,若 a $→$ b,b $→$ c,也有 a $→$ c。如果 a $↛$ b,并且 b $↛$ a,则表明 a 和 b 是并发的。
The Partial Ordering(偏序关系): 由 happened before 延伸而来,但是偏序关系的范围大于 happened before。
- 偏序关系包括 happened before 关系。
- 如果 a $→$ b,并且 a $→$ c,但 b 和 c 之间没有任何关系,表明我们可以不在乎 b 和 c 的顺序,将其看做是并发的。
为了在分布式系统中实现对所有事件进行排序,论文引入了逻辑时钟,我们能将其看做是一个自增的 id,可以不依赖物理时钟,只需要为每个进程定义事件顺序即可,我们可以用 $C_i(a)$ 定义为第 i 个进程的事件 a 的逻辑时钟。
对于任意事件 a 和 b,如果 a $→$ b,则 C(a) < C(b),那么需要满足:
- IR1:对于单进程,每个进程定义的逻辑时钟需要自增。
- IR2:对于多进程,在传递事件(a)时,进程 i 可以附带上自己的逻辑时钟 $C_i(a)$,进程 j 接收到事件(b)时,可能需要根据其大小控制自己的逻辑时钟 $C_j(b)$ 不小于 $C_i(a)$。
通过逻辑时钟,我们就可以定义全序关系 $⇒$:
- $C_i(a) < C_j(b)$。
- $C_i(a) < C_j(b)$ 并且 $process_i < process_j$。
可以知道的是,$⇒$ 关系最终构成的事件顺序是不唯一的,受到逻辑时钟的影响;$→$ 则是唯一的。 那么如何才能定义全序关系,并且实现呢?对于一个资源互斥的使用场景,全序关系的算法实现需要满足三个条件:
- 同一时间只能有一个进程持有某个资源,进程持有资源的时候,前一个使用此资源的进程需要释放资源。
- 不同资源的请求必须按顺序执行。
- 如果每个已经使用的资源最终被释放,那么每个资源的请求都会被通过。
具体来说,可以用五个规则来实现:
- 进程 i 请求资源时,需要发送包含时间戳和进程 id 的请求到其他所有对等点,并且在自己的请求队列添加这个请求。
- 进程 j 收到请求后,需要返回一个回复或请求,要求时间戳更大。
- 当进程需要释放资源的时候,先删除请求队列里面所有当前资源的请求,再发送删除的请求到所有其他节点。
- 当其他进程收到了删除请求后,直接移除请求队列中所有对应资源的请求。
- 进程持有资源需要满足以下两个条件:① 请求队列中对应资源的请求在开头;② 进程收到了其他所有进程的时间戳大于当前请求的消息。
但是假如用户 A 发送了一个请求到服务器,告知用户 B 之后用户 B 也发送了同样的请求,如果全序关系分配了较大的逻辑时钟给 B 的请求,那么服务器返回的结果可能与期望的结果不符。
这个问题,并不是系统内部能够控制的,虽然定义的全序关系可以组成一个多个进程统一的命令队列,每个机器按照队列执行可以保证分布式系统的一致性,但是,集群无法感知外界的“happened before”关系,可能会得到违反自觉的结果。
因此,本文的作者引入了物理时钟解决这个问题,引入物理时钟相当于在现实世界添加了一个强约束:如果 $a \rightarrow b$ 则必定 $C(a) < C(b)$。
对于时钟约束,我们假设每个机器的速率可以用 $dC_i(t)/dt$ 表示,现实的物理时钟的速率为 1,集群的时钟有两个限制:
- PC1: $| dC_i(t)/dt -1| < \kappa$ $\kappa$ 常 $ \leq 10^{-6}$
- PC2: $|C_i(t) - C_j(t)| < \epsilon$
因为两个机器的时钟不可能以同一个速率运行,那么两者的差值会越来越大,我们必须设计一个时钟同步算法来保证 $a \nrightarrow b$ 的时候满足以上两个约束(现实世界的强限制)。
首先需要考虑的问题是如何设定 $κ$ 和 $ϵ$ 的值,假定所有机器的时间只会向前流逝,也就是说调整时钟的时候只能调的更慢而不能调快。首先想到的是这两个值与集群进程间通信的时延有关,如果进程间通信的最小时延为 $μ$,由限制 1,我们可以得到 $C_i(t+μ) - C_i(t) > (1-κ)μ$,左边是同一机器在经过 $μ$ 时间后机器时钟的时间差,理想情况下应该等于 $μ$,但是因为时钟只会更慢于物理时钟,因此其值必定小于 $μ$;右边则是允许机器时钟与物理时钟的最小的误差;由限制 2,我们可以得到 $C_i(t+μ)-C_j(t)>0$,化简可知 $ϵ/(1-κ)≤μ$。
在真正的物理世界,我们由上面的 逻辑时钟限制 衍生出了以下两个限制:
- IR1':对于每个进程 i,如果在物理时间 t 的时候没收到请求,在时间 t 的时候 $C_i$ 是连续可导的,并且 $dC_i(t)/dt > 0$
- IR2':进程 i 发送信息 m 的时候需要附带自己的时间戳 $T_m = C_i(t)$,进程 j 接收到 m 的时候为 t',那么 j 需要设置自己的时钟为 $max{C_j(t'-0),T_m+μ_m}$。
基于以上的两个限制,论文通过两页纸的证明得出结论:对于一个分布式系统,可以将节点之间的通信表示成有向图的边。如果有向图的直径为 d,并且对于每个请求 m,$μ_m≤μ$,就能满足 PC1;再有 IR1’和 IR2',令每条边每隔 $\tau$ 秒在小于 $\xi$ 的延迟下发送请求的情况下 $ϵ \approx d(2κτ+ξ),μ+ξ\ll τ$。