1. 为什么要引入逻辑时钟(Logic Clock)?
在分布式系统中,不同的机器的时间可能不一样,使用时间戳(timestamp)的方式会导致产生误差。也许你会说让机器定期进行 NTP 时间同步,但是在一个集群中,不同机器内部时间计算也会产生误差,可能有些机器时间前进的快点,有些机器会慢点,这种现象也叫时钟漂移(Clock Drift)。
2. 如何实现逻辑时钟(Logic Clock)?
Logic Clock 中最出名的就是 Lamport Timestamp。通过逻辑时间,我们可以判断不同事件的因果顺序关系。
Lamport Timestamp 算法的实现遵循以下规则:
- 每一台机器内部都有一个时间戳(Timestamp),初始值为 0。
- 机器执行一个事件(event)之后,该机器的 timestamp + 1。
- 当机器发送信息(message)给另一台机器,它会附带上自己的时间戳,如 <message, timestamp> 。
- 当机器接受到一条 message,它会将本机的 timestamp 和 message 的 timestamp 进行对比,选取大的 timestamp 并 +1。
首先规定一个事件 A 的逻辑时间戳表示方式为 C(A)。下面所有的时间戳都为逻辑时间戳,不是机器真实时间。
如果事件 A 在事件 B 之前发生,叫做 happened-before,表示为 A->B,那么事件 A 的时间戳一定小于事件 B。即表达为:
但是我们要注意这种推导关系是不能反过来的:
比如上图 Q 中的事件 3 和 R 中的事件 1,虽然 3>1,但是它们之间没有因果关系,它们是并行 (concurrent) 的或者是独立的(independent)。
我们还可以知道,如果 C(A) <= C(B) ,那么事件 B 是绝对不可能发生在事件 A 之前:
当然,如果事件 A 和 事件 B 的时间戳相同,则它们是并行或独立的,即
3. 为什么要引入向量时钟(Vector Clock)?
但是 Lamport Timestamp 不能很好的满足分布式系统,比如你不能区分两个事件是否有关联,或者在一个多点读的 key-value 数据库中,你无法确定保存哪一份副本(通常保存最新的那份副本)。
如下图所示,你其实无法单纯的通过 logic clock 比较来判断 Process 1 中的事件 3 和 Process 2 中的事件 8 是否有关系。
如果事件 3 执行时间延迟几秒,这不会影响到事件 8。所以两个事件互不干涉,为了判断两个事件是否为这种情况,我们引入了 Vector Clock(向量逻辑时间)。
4. 如何实现向量时钟(Vector Clock)?
Vector Clock 是通过向量(数组)包含了一组 Logic Clock,里面每一个元素都是一个 Logic Clock。如上图,我们有 3 台机器,那么 Vector Clock 就包含三个元素,每一个元素的索引(index)指向自己机器的索引。
我们遵循以下规则:
- 每一台机器都初始化所有的 timestamp 为 0。例如上面的例子,每一台机器初始的 Vector Clock 均为
[0, 0, 0]
。- 当机器处理一个 event,它会在向量中将和自己索引相同的元素的 timestamp + 1。例如 1 号 机器处理了一个 event,那么 1 号机器的 Vector Clock 变为
[1, 0, 0]
。- 每当发送 message 时,它会将向量中自己的 timestamp + 1,并附带在 message 中进行发送。如 <message, vector> 。
- 当一台机器接收到 message 时,它会把自己的 Vector Clock 和 message 中的 Vector Clock 进行逐一对比(每一个 timestamp 逐一对比),并将 timestamp 更新为更大的那个(类似于 Lamport Timestamp 的操作)。然后它会对代表自己的 timestamp + 1。
由此我们也可以知道,如果 A->B ,那么 A 的 Vector Clock 中的每一个元素都会小于 B 中的每一个元素。
判断两个事件是否并行或独立
回到刚刚判断两个事件是不是有关联,我们可以看到 Process 1 中的 [3, 0, 0]
和 Process 2 中的 [2, 4, 2]
,其中 3 > 2 而 0 < 4,0 < 2 。有些 timestamp 大于对方,而有些 timestamp 又小于对方,由此我们可以得知这两个事件是互不相干的。
以下是另一个例子:
5. 逻辑时钟和向量时钟的习题例子
请标出下图中各个事件的 Lamport 逻辑时钟和向量时钟。
逻辑时钟:
回顾 Lamport Timestamp 算法的实现规则:
- 每一台机器内部都有一个时间戳(Timestamp),初始值为 0。
- 机器执行一个事件(event)之后,该机器的 timestamp + 1。
- 当机器发送信息(message)给另一台机器,它会附带上自己的时间戳,如 <message, timestamp> 。
- 当机器接受到一条 message,它会将本机的 timestamp 和 message 的 timestamp 进行对比,选取大的 timestamp 并 +1。
向量时钟:
回顾向量时钟的实现规则:
我们有 n 台机器,那么 Vector Clock 就包含 n 个元素,每一个元素的索引(index)指向自己机器的索引。
- 每一台机器都初始化所有的 timestamp 为 0。
- 当机器处理一个 event,它会在向量中将和自己索引相同的元素的 timestamp + 1。
- 每当发送 message 时,它会将向量中自己的 timestamp + 1,并附带在 message 中进行发送。如 <message, vector> 。
- 当一台机器接收到 message 时,它会把自己的 Vector Clock 和 message 中的 Vector Clock 进行逐一对比(每一个 timestamp 逐一对比),并将 timestamp 更新为更大的那个(类似于 Lamport Timestamp 的操作)。然后它会对代表自己的 timestamp + 1。
参考链接:https://www.jianshu.com/p/081ccf63fe6