Lamport 时钟之前一直似懂非懂,今天看了 Martin Kleppmann 的 教学视频,觉得自己又行了。
因果关系与物理时钟
假设你发了朋友圈,有两个朋友评论:
- A 说:“这是在北京吧”
- B 回复 A 说:“应该不是,看着像上海”
我们人肉能识别出两句话之间的因果关系:#A
是因,#B
是果,但是计算机怎么判断呢?
一种思路是给评论加上生成时间,比如 #A_10:01
, #B_10:02
,系统按时间对评论排序,就能判断 #B
发生成 #A
之后。这个方法逻辑上没问题,但现实中没有一种可靠的方法,能准确地同步各个机器上的时间(也称为物理时间)。于是可能出现下面的情况:
处理 B
评论的机器时钟慢了,导致 B
评论的时间戳更小,系统排序时把 #B
放在了前面,因果错乱。
Lamport 时钟
Lamport 时钟[1]是一种逻辑上的机制,用来给各个事件打标签,保证如果事件 A
发生于 B
之前,则 A
的标签 L(A)
一定小于 B
的标签 L(B)
。
具体要怎么做呢?每个机器各自维护一个计数器 t
,然后:
- 初始化时,每个机器都把
t
置为0
- 本机产生一个事件时,先执行
t = t+1
,再用自增后的t
来标记事件 - 要发送一个事件时,执行
t = t+1
,并发送(t, m)
,即把计数器和事件都发出去 - 接收到一个事件
(t', m)
时,则需要更新本地的计数器t = max(t, t') + 1
,并把m
发送到本地
于是如果使用这个算法,则上面朋友圈的例子就变成了:
可以看到事件 (4, 这是北京吧)
发生在 (6, 应该不是)
之前,它们的标签 t
能反映出这一点。
Lamport 时钟的局限
为什么 Lamport 时钟能体现事件发生的“因果”关系?如果两个事件有“因果”,它们一定是有“同步”的操作,而 Lamport 时钟则是在“同步”时(第 #4 点),通过 max(t, t')
同步了二者的逻辑时间。
由于 A-Before
的事件满足 t <= T
,而 B-After
的事件满足 t >= T+1
,所以能保证 A-before <= T < T+1 <= B-after
,而 B-after
中的事件逻辑上是发生成
A-before
的事件之后的,且标签 t
也满足先后关系,因此保证了因果顺序。
但是在上图中,我们虽然推出 A-before < B-after
,但其它几个区域发生的事件就没法有确定的对比结论了。例如所有 B-before
中的事件,一定发生成 A-after
中的事件之前吗?(B-before < A-after
),细想一下会发现并没有办法得出这个结论。明确可比的有这几个区域:
A-before < A-after
,A 机事件发生的先后决定B-before < B-after
,B 机事件发生的先后决定A-before < B-after
,A、B 之间的因果性决定
从另一个角度看,Lamport 时钟可以保证如果事件 a < b
(a
发生在 b
之前),就可以推出它们的标签满足 L(a) < L(b)
。但反过来,如果看到两个标签 L(a) < L(b)
,能反推出 a < b
吗?其实是不行的,因为我们能判定的只有 A-before
和
B-after
两个区域的事件,但只看 L(a)
和 L(b)
我们并不知道 a
和 b
落在哪个区域,因此无法判断 a
和 b
发生的先后。这就是 Lamport 时钟的局限性,
Vector 时钟
vector 时钟可以解决这个问题:如果两个事件落在可比较的区域,则通过对比 vector
时钟产生的标记,可以得出对应事件发生的先后顺序,即通过 L(a) < L(b)
可以得出
a < b
的结论。那 vector 时钟是怎么做到的?
- 假设有 N 台机器,记为
N[1], N[2], ..N[n]
- 每台机器需要维护一个 N 维向量作为计数器,记为
T = <t1, t2, ..., tn>
N[i]
本机产生一个事件时,就把本机向量里的ti
递增,即T[i]++
- 机器
N[i]
发送消息m
时,先执行T[i]++
,再发送(T, m)
- 机器
N[j]
收到消息(T', m)
时,执行T = max(T, T')
,再执行T[j]++
这些规则看起来很复杂,但实际上它和 Lamport 时钟的“同步逻辑”一样,只是每个节点都保存了其它所有节点,最后一次同步过的计数器。执行起来如下图[2]:
Vector 时钟最后的标签有多维,如何比较呢?vector 时钟要求,如果每一维上,都有
T[i] < T'[i]
,则认为 T < T'
;如果每一维都有 T[i] = T'[i]
,则认为 T = T'
;其它情况,都认为 T
和 T'
不可比。
条件 a < b
推出 T(a) < T(b)
的结论是比较简单的,与 Lamport 时钟类似,这里给个图,不多说明了:
从 T(a) < T(b)
反推 a < b
呢?其实从 T
的定义来看,可以理解成 T
代表的是当前事件及之前发生的所有事件的集合,而 T(a) < T(b)
可以等价于集合的从属关系,那么事件 a
一定包含在 T(b)
里,因此 a < b
[3]。
小结
Lamport 时钟解决的是分布式系统下的因果一致性问题,方式是在多机有交互时求计数器的 max
。它的局限是无法从计数器的大小反推事件的先后顺序。
Vector 时钟基本思路和 Lamport 时钟一样,但它在每个机器上都维护了最后看到的,其它机器的计数器。
Lamport Clock,也称为 Lamport Timestamp,以发明者 Leslie Lamport 命名,Lamport 也是著名的 Paxos 的发明者。 ↩
注意这张图和上面 lamport 时钟的示例,算法的细节上有简化,收到信息时没有递增计数器 ↩
写到这里的时候受到知识的诅咒了,不管是从图像来看,还是从集合的视角来看,都太显然了,如果读者没理解的话,推荐看 Martin Kleppmann 的教程,说得比我明白。当然他的教程里有数学表示,更精确。 ↩