事件之间因果关系的概念是并行、分布式计算和操作系统设计和分析的基础。通常情况,使用物理时间(physical time)跟踪因果关系。但是,在分布式系统中,不可能有全局物理时间,实际的可能仅是它的近似。
As asynchronous distributed computations make progress in spurts, it turns out that the logical time, which advances in jumps, is sufficient to capture the fundamental monotonicity property associated with causality in distributed systems. 随着分布式计算的迅猛发展,出现了以跳跃方式行进的逻辑时间的概念,它能充分捕获与分布式系统因果关系有联系的基本的单调性特性。
本章讨论实现逻辑时间的三种方法(即标量时间、矢量时间和矩阵时间)以捕获分布式计算时间之间的因果关系。
有关进程事件之间因果优先关系的知识有助于解决分布式系统的各种问题:
在一个逻辑时钟系统中,每一个进程都有一个按规则运行的逻辑时钟。每个事件都赋予一个时间戳(timestamp),事件之间的因果关系一般从它们的时间戳推导出来。赋予事件的时间戳遵守基本单调性。这就是说,事件a的因果性影响事件b,那么α的时间戳就比b的时间戳小。
本章将首先提出分布式系统中逻辑时钟系统的一个一般性框架,然后讨论在一个分布式系统中实现逻辑时间的主要方法。第一种方法是Lamport标量时钟,其时间用非负整数表示;第二种方法,其时间用一个非负整数的向量表示;第三种方法,时间用一个非负整数的数组表示。我们也讨论有效实现向量时钟系统的方法。
本章以讨论虚拟时间作为结尾,虚拟时间的实现是使用时间变形(time-warp)机制,并简要说明物理时钟同步和网络时间协议。
逻辑时钟系统由一个时间域(time domain)
T
T
T 和一个逻辑时钟
C
C
C 组成。
T
T
T 的元素形成一个关系
<
<
< 上的偏序集合(partially ordered set)。这种关系通常称作先发生或因果优先,从直观上来说,这种关系类似于物理时间的早于关系。逻辑时钟
C
C
C 是一个函数,他把分布式系统中的事件
e
e
e 影射到时间域
T
T
T 中的一个元素,表示为
C
(
e
)
C(e)
C(e) 且成为
e
e
e 的时间戳,定义如下:
C
:
H
↦
T
C:H\mapsto T
C:H↦T
使得下面的性质被满足:
偏序:设 R 为非空集合 A 上的关系,如果 R 是自反的、反对称的和可传递的,则称 R 为 A 上的偏序关系,简称偏序
实现逻辑时钟需要解决两个问题:本地每个进程表示逻辑时间的数据结构和更新数据结构以保证一致性条件的协议(规则集合)。
每个进程 p i p_i pi 维护数据结构使其有下面两种能力
协议保证对进程本地时钟(也就是以它的角度所见的全局时间)进行一致地管理。它由下面两个规则组成:
(1) R1规则。此规则控制进程在执行一个事件(发送、接收或内部的事件)时如何更新本地逻辑时钟。
(2) R2规则。此规则管理流程如何更新其全局逻辑时钟,以更新其全局时间和全局进度视图。它规定了关于逻辑时间的哪些信息承载在消息中,以及接收进程如何使用该信息更新其全局时间视图。
1978年Lamport[9]提出了标量表示法,以便对分布式系统中的事件进行全排序。在该表示法中的时间域是非负整数集合。进程 p i p_i pi 的本地逻辑时钟和本地所见的全局时间被挤入一个整数变量 C i C_i Ci 。
R1规则和R2规则更新时钟如下:
R1:在执行一个事件之前,进程
p
i
p_i
pi 执行如下动作:
C
i
:
=
C
i
+
d
d
>
0
C_i:=C_i +d\quad d>0
Ci:=Ci+dd>0
一般来说,每次执行R1规则,d就可能有不同的值,且这个值往往依赖于不同的应用。典型的d值保持为1,这足以把一个进程中每个事件的时间标识为唯一的,且d的增量保持最低的级数。
R2:每个消息附加有它的发送方在发送时的时钟值,当进程 p i p_i pi 接收到一个带有时间戳 C m s g C_{msg} Cmsg 的消息时,它执行如下动作:
图3.1显示了d=1的标量时间的演化过程。

图3.1 标量时间变化
对两个事件
e
i
e_i
ei 和
e
j
e_j
ej,
e
i
→
e
j
⇒
C
(
e
i
)
<
C
(
e
j
)
e_i\to e_j\Rightarrow C(e_i)
标量时钟能用于排列分布式系统的全序事件。全序事件的主要问题是在不同进程中可能会有两个或两个以上的事件有相同时间戳(对两个事件
e
i
e_i
ei 和
e
j
e_j
ej,
C
(
e
i
)
=
C
(
e
j
)
⇒
e
i
∣
∣
e
j
C(e_i)=C(e_j)\Rightarrow e_i|| e_j
C(ei)=C(ej)⇒ei∣∣ej)。例如,在图3.1中,进程
p
1
p_1
p1 的第三个事件和进程
p
2
p_2
p2 的第二个事件有相同的标量时间戳。因此,为对这样的事件排序,需要一个打破均衡的机制。典型的情形可按下述方法进行额外比较:进程标识符按线性排序并且有相同标量时间戳事件之间的相等关系以过程标识符为根据被打破。进程标识符排位越低,则优先级越高。一个事件的时间戳用
(
t
,
i
)
(t,i)
(t,i)对表示,其中
t
t
t 是发生时间,
i
i
i是它在进程的标识。各带有时间戳
(
h
,
i
)
(h,i)
(h,i) 和
(
k
,
i
)
(k,i)
(k,i) 的两个事件
x
x
x 和
y
y
y 的全序关系
≺
\prec
≺ 定义如下:
x
≺
y
⇔
(
h
<
k
o
r
(
h
=
k
a
n
d
i
<
j
)
)
x\prec y\Leftrightarrow(h
这里需要说明:KaTeX parse error: Undefined control sequence: \or at position 29: …tarrow x\to y\ \̲o̲r̲ ̲\ x||y
如果 d d d 的增加值总是1,则标量时间有下面有趣的性质:如果事件 e e e 有一个 h h h 的时间戳,那么 h − 1 h -1 h−1 表示在产生事件 e e e 之前,所需要的以事件为计数单位的最小逻辑持续时间,我们称其为事件 e e e 的高度。换句话说,在事件 e e e 产生之前,已顺序产生了 h − 1 h-1 h−1 个事件,而不管这些事件是哪些进程产生的。例如,图3.1中,在以 b b b 为终点的最长因果路径(the longest causal path)上,有5个事件先于事件 b b b。
例如,在图3.1中进程 p 1 p_1 p1 的第三个事件的标量时间戳比进程 p 2 p_2 p2 的第三个事件小,而前者并没有在后者之前发生。
标量时钟不是强一致性的原因是:一个进程的本地逻辑时钟和本地全局时钟被压缩成一个,导致了不同进程的事件之间因果依赖信息的缺失(the loss causal dependency information among events at different processes.)。例如,图3.1中,进程 p 2 p_2 p2 接收到进程 p 1 p_1 p1 的消息时,它更新它的时钟为3,忘掉它所依赖的 p 1 p1 p1 上的最近事件的时间戳2。
向量时钟系统由Fidge、Mattern和Schmuck独立开发出来,在向量时钟系统中,时间域由 n n n 维非负整数向量集合表示。每个进程 p i p_i pi 维护一个向量 v t i [ 1 … n ] vt_i[1…n] vti[1…n],其中 v t i [ i ] vt_i[i] vti[i] 是 p i p_i pi 的本地逻辑时钟并描述在进程 p i p_i pi 上逻辑时间的进度。 v t i [ j ] vt_i[j] vti[j] 表示进程 p i p_i pi 的有关进程 p j p_j pj 本地时间最近的信息。如果 v t i [ j ] = x vt_i[j]=x vti[j]=x 那么 p i p_i pi 就知道进程 p j p_j pj 的逻辑时间已进展到 x x x 。用整个向量 v t i vt_i vti 代替 p i p_i pi 所见的全局时间,并且用于给事件打上时间戳。
进程 p i p_i pi 使用下面的 R1 规则和 R2 规则更新它的时钟
R1:在执行一个事件之前,进程
p
i
p_i
pi 更新其本地逻辑时间如下:
v
t
i
[
i
]
:
=
v
t
i
[
i
]
+
d
d
>
0
vt_i[i]:=vt_i[i] +d\quad d>0
vti[i]:=vti[i]+dd>0
R2:把每个消息 m m m 加入到发送方进程在发送时的向量时钟 v t vt vt。一旦接收到这样一个消息 ( m , v t ) (m,vt) (m,vt) ,进程 p i p_i pi 执行如下一系列动作:
更新它的全局逻辑时间如下
1
≤
k
≤
n
:
v
t
i
[
k
]
:
=
m
a
x
(
v
t
i
[
k
]
,
v
t
[
k
]
)
1\le k\le n:\ vt_i[k]:=max(vt_i[k],vt[k])
1≤k≤n: vti[k]:=max(vti[k],vt[k])
执行R1规则
传递该消息
一个事件的时间戳是该事件执行时它的进程的向量时钟的值。图3.2显示了增量 d = 1 d=1 d=1 的向量时钟进度的例子。最初,向量时钟是 [ 0 , 0 , 0 , … . , 0 ] [0,0,0,….,0] [0,0,0,….,0]。

定义下面的关系以比较两个向量时间戳 v h vh vh 和 v k vk vk:

如果分布式系统的事件用向量时钟系统来打时间戳,就具有如下的性质:
如果发生一个事件的过程是已知的,那么比较两个时间戳的测试能被简化如下:如果事件
x
x
x 和
y
y
y 分别发生在进程
p
i
p_i
pi 和
p
j
p_j
pj 并且分别给它们赋时间戳
v
h
vh
vh 和
v
k
vk
vk,那么
KaTeX parse error: Undefined control sequence: \and at position 72: … (vh[i]>vk[i] )\̲a̲n̲d̲ ̲(vh[j]
向量时钟系统具有强一致性。因此,通过查看两个事件的向量时间戳,我们就能确定这两个事件是否有因果关系。总之,Charron-Bost指出,为具有这种性质,向量时钟的维数(也就是分布式计算中进程的总数)不能小于n。
如果在R1规则中d总是1,那么在进程 p i p_i pi 中向量的第i个元素 v t i [ i ] vt_i[i] vti[i]表示在 p i p_i pi 上知道该瞬间之前发生的事件数。这样,如果事件 e e e 有时间戳 v h vh vh ,那么 v h [ j ] vh[j] vh[j] 就表示进程 p j p_j pj 执行的因果关系先于 e e e 的事件数。 ∑ v h [ j ] − 1 ∑vh[j]-1 ∑vh[j]−1表示在分布式计算中因果关系先于 e e e 的事件总数。
由于向量时间准确跟踪了因果依赖关系,因此它有各种广泛的应用。例如,应用于分布式查错,实现因果排序的通信和因果分布式共享存储。建立全局断点,在最佳恢复中确定检查点的一致性。
Singhal-Kshemkalyani的差量技术是基于这样的观察结果:在向同一进程连续发送消息之间,发送方进程的向量时钟中只有少数条目可能会更改,因为在传递消息时,仅有向量时钟中的某些项会频繁的交互作用,所以当进程的数量巨大时,上述情况更有可能出现。在这种技术中,当进程 p i p_i pi 向进程 p j p_j pj 发送一条消息时,它只携带与最后一条发送给 p j p_j pj 的消息不同的向量时钟项。
该技术的工作原理如下:从上次发送给
p
j
p_j
pj 消息以来,
p
i
p_i
pi 向量时钟的项
i
1
,
i
2
,
⋯
,
i
n
1
i_1,i_2,⋯,i_{n1}
i1,i2,⋯,in1 已经更新为
v
1
,
v
2
,
⋯
,
v
n
1
v_1,v_2,⋯,v_{n1}
v1,v2,⋯,vn1,则进程
p
i
p_i
pi 可把
{
(
i
1
,
v
1
)
,
(
i
2
,
v
2
)
,
⋯
,
(
i
n
1
,
v
n
1
)
}
\{(i_1,v_1),(i_2,v_2),⋯,(i_{n1},v_{n1})\}
{(i1,v1),(i2,v2),⋯,(in1,vn1)}这样的压缩时间戳附加在发往
p
j
p_j
pj 的消息中。当
p
j
p_j
pj 接收到消息时,更新它的向量时钟:
v
t
i
[
i
k
]
=
m
a
x
(
v
t
i
[
i
k
]
,
v
k
)
,
k
=
1
,
2
,
.
.
.
,
n
1
vt_i[i_k]=max(vt_i[i_k],v_k),\ k=1,2,...,n_1
vti[ik]=max(vti[ik],vk), k=1,2,...,n1
这个技术的实现要求每个进程记住它上一次发给其他所有进程消息的向量时间戳,该技术的直接实现将导致在每个进程上有
O
(
n
2
)
O(n^2)
O(n2) 的存储开销,为传递消息,该技术同样要求通信通道遵守
F
I
F
O
FIFO
FIFO规则。
Singhal和Kshemkalyani开发了一个聪明的技术,该技术把在每个进程上的存储开销削减到 O ( n ) O(n) O(n)数量级,以如下方式工作。进程 p i p_i pi 维护下面两个附加的向量:
L S i [ 1... n ] LS_i[1...n] LSi[1...n](‘Last Sent’):
L S i [ j ] LS_i[j] LSi[j]表示当进程 p i p_i pi 最后一次向进程 p j p_j pj 发送消息时 v t i vt_i vti 的值。
L U i [ 1... n ] LU_i[1...n] LUi[1...n](‘Last Update’):
L U i [ j ] LU_i[j] LUi[j]表示当进程 p i p_i pi 最后一次更新 v t j vt_j vtj 时 v t i vt_i vti 的值。
当进程
p
i
p_i
pi 接收到消息后,捕获到进程
p
j
p_j
pj 新进展后,更新
v
t
i
[
j
]
vt_i[j]
vti[j] ,
L
U
i
[
j
]
LU_i[j]
LUi[j] 也将更新。当进程
p
i
p_i
pi 向
p
j
p_j
pj 发送消息时,
L
S
i
[
j
]
LS_i[j]
LSi[j]将更新。如果从上次
p
i
p_i
pi 到
p
j
p_j
pj 通信以来,向量时钟只有
v
t
i
[
k
]
vt_i[k]
vti[k]被更新,那么
L
S
i
[
j
]
<
L
U
i
[
k
]
LS_i[j]
{
(
x
,
v
t
i
[
x
]
)
∣
L
S
i
[
j
]
<
L
U
i
[
j
]
}
\{(x,vt_i[x])\ |\ LS_i[j]
作为一个时间戳给
p
j
p_j
pj , 而不是发送消息中的n项向量。这样,并不是大小为的整个向量和消息一起发送,而仅是从上次发送给那个进程消息以来向量时钟中已经改变的那些元素以
{
(
p
1
,
l
a
t
e
s
t
_
v
a
l
u
e
)
,
(
p
2
,
l
a
t
e
s
t
_
v
a
l
u
e
)
,
…
}
\{(p_1,latest\_value),(p_2,latest\_value),…\}
{(p1,latest_value),(p2,latest_value),…}
格式发送,其中
p
i
p_i
pi 表示向量时钟的第
p
i
p_i
pi 个成分已改变。
这种压缩的方法可以用图3.4显示,例如 p 3 p_3 p3 到 p 2 p_2 p2 的第三个消息(包含时间戳{(3,2)})通知 p 2 p_2 p2 ,向量时钟的第三个成分已经被修改,且新的值为2。这是因为进程 p 3 p_3 p3 自发送给 p 2 p_2 p2 的最后一条消息以来,已将时钟值从1提高到2。

图3.4
使用这种技术,能实质性地减少在大的系统中维护向量时钟的开销,尤其是进程的相互作用表现出瞬时性和空间局部性时。这项技术在包括分布式因果存储共享、分布式死锁检测、互斥的执行及局部化通信等各种应用中都是有用的。在分布式系统中,这些都是显而易见的。
该技术通过在一条消息中只传递一个标量值来减少消息的大小。
每个进程
p
i
p_i
pi 维护一个依赖向量
D
i
D_i
Di,初始化为:
D
i
[
j
]
=
0
f
o
r
j
=
1
,
.
.
.
,
n
D_i[j]=0\ for\ j=1,...,n
Di[j]=0 for j=1,...,n
D
i
D_i
Di被更新如下:
这样,依赖向量 D i D_i Di 仅仅反映直接依赖,在任何瞬间, D i [ j ] D_i[j] Di[j] 表示在进程 p j p_j pj 上直接影响当前状态的上次事件的序列号。
图3.5展示了Fowler-Zwaenepoel技术。例如,当进程p4向进程p3发送一条消息时,它携带了一个标量,表示由于这条消息,p3对p4的直接依赖关系。随后,进程p3向进程p2发送了一个消息,该消息附带了一个标量,表明p2因为这个消息而直接依赖于p3。现在,进程p2实际上间接依赖于进程p4,因为进程p3依赖于进程p4。然而,进程p2从未被告知它对p4的间接依赖。
图3.5 用Fowler-Zwaenepoel计算的向量时钟进展
虽然传递(间接)依赖关系并没有通过这种方式维护,这些依赖关系能通过脱机的递归跟踪事件的直接依赖向量来获得。这个技术对于可以通过离线方式计算因果关系的应用来说是理想的,像因果断点(causal breakpoints)和异步检测点的恢复(asynchronous checkpoint recovery)。
Fowler-Zwaenepoel直接依赖技术并不能在进程执行期间实时捕获传递依赖关系。而且,一个进程在接收一个消息之后而不是在发送出任何消息之前,必须观察一个事件(更新和记录它的依赖关系向量)。否则,依据直接依赖向量重构一个向量时间戳期间,将不能捕获全部的因果依赖关系。如果事件的发生非常频繁,该技术将要求记录大量事件的历史。
在Jard-Jourdan的技术[8]中,可以自适应地观察事件,同时保持检索所观察事件的所有因果依赖性的能力。观察一个事件意味着记录关于其依赖关系的信息。
这个方法使用了这样的思想:当一个被观察的事件记录它的依赖关系时,跟随其后发生的这些事件就能够确定它们的传递依赖关系。这就是说, 间接依赖该事件的集合使用了记录有关 e e e 的信息
在一个矩阵时钟系统中,用一个非负整数 n × n n×n n×n 的集合表示时间。其中,进程 p i p_i pi 维护一个矩阵 m t i [ 1.. n , 1. n ] mt_i[1..n,1.n] mti[1..n,1.n],
整个矩阵 m t i mt_i mti表示本地所见的全局逻辑时钟, 一个事件的矩阵时间戳是当一个事件执行时,进程矩阵时钟的值 。
进程 p i p_i pi 使用下面的 R1 规则和 R2 规则更新它的时钟
R1:在执行一个事件之前,进程
p
i
p_i
pi 更新其本地逻辑时间如下:
m
t
i
[
i
,
i
]
:
=
m
t
i
[
i
,
i
]
+
d
d
>
0
mt_i[i,i]:=mt_i[i,i]+d\quad d>0
mti[i,i]:=mti[i,i]+dd>0
R2:每个消息 m m m 中装入矩阵时间 m t mt mt。一旦接收到这样一个消息 ( m , m t ) (m,mt) (m,mt) ,进程 p i p_i pi 执行如下一系列动作:
更新它的全局逻辑时间如下
执行R1规则
传递该消息
[1] Ajay D. Kshemkalyani, & Mukesh Singhal (2008). Distributed Computing: Principles, Algorithms, and Systems