Lamport 逻辑时钟(Lamport Timestamp)和 Vector Clock 简单理解

原文地址:https://www.inlighting.org/archives/lamport-timestamp-vector-clock

Lamport 逻辑时钟(Lamport Timestamp)

Lamport Timestamp 是一种衡量时间和因果关系的方法。现实生活中,很多程序都有着因果(causality)关系,比如执行完事件 A 后才能执行事件 B。

int main {
  create_photos(6);
  view_photos(6);
  return 0;
}

比如上面的代码,我只有创建完照片 6 才能访问照片 6,这就是因果关系。但是在分布式系统中,我们如何衡量事件的因果顺序呢?

如下图,比如我有 A,B,日志服务器 三台机器,用户发起一个购买操作,首先第一个请求在 Server A 上面创建了订单,之后支付操作在 Server B 上进行。然后为了记录用户操作,Server A 和 Server B 异步发送日志写入请求到达日志服务器。假设两条日志同时到达,那么日志服务器该如何区分先后顺序呢?

2020-05-03-FQRWHv

我们首先想到的就是使用时间戳(timestamp)的方式,根据时间戳的大小来判断事情发生的先后。但是在分布式系统中,不同的机器的时间可能不一样,这样导致这种方法会产生误差。也许你会说让机器定期进行 NTP 时间同步,但是在一个集群中,不同机器内部时间计算也会产生误差,可能有些机器时间前进的快点,有些机器会慢点,这种现象也叫 Clock Drift

Logic Clock

所以我们要引入逻辑上面的时间,其中 Logic Clock 中最出名的就是 Lamport Timestamp。通过逻辑时间,我们可以判断不同事件的因果顺序关系。

算法实现

Lamport Timestamp 算法的实现遵循以下规则:

  • 每一台机器内部都有一个时间戳(Timestamp),初始值为 0。
  • 机器执行一个事件(event)之后,该机器的 timestamp + 1。
  • 当机器发送信息(message)给另一台机器,它会附带上自己的时间戳,如 <message, timestamp>
  • 当机器接受到一条 message,它会将本机的 timestamp 和 message 的 timestamp 进行对比,选取大的 timestamp 并 +1。

有些会说计数器叫 counter,也有些会说叫 timestamp,反正都代表一种计数方式。

Lamport Timestamp 伪代码如下:

发送信息

void sendMessage() {
  do_one_event();
  timestamp = timestamp + 1;
  send(message, timestamp);
}

接收信息

void receiveMessage() {
  (message, remote_timestamp) = receive();
  counter = max(timestamp, remote_timestamp) + 1;
}

思考

2020-05-01-BHuFXF

首先规定一个事件 A 的逻辑时间戳表示方式为 C(A) 。下面所有的时间戳都为逻辑时间戳,不是机器真实时间。

如果事件 A 在事件 B 之前发生,(叫做 happened-before,表示为 A\rightarrow B),那么事件 A 的时间戳一定小于事件 B。即表达为:

A\rightarrow B \Rightarrow C(A)<C(B)

但是我们要注意这种推导关系是不能反过来的。

C(A)<C(B)\nRightarrow A\rightarrow B
比如上图 Q 中的事件 3 和 R 中的事件 1,虽然 3>1 但是它们之间没有因果关系,它们是并行 (concurrent) 的(或者是独立的,independent)。

我们还可以知道,如果 C(A) <= C(B) ,那么事件 B 是绝对不可能发生在事件 A 之前。
C(A)<=C(B)\Rightarrow B\nrightarrow A
当然,如果事件 A 和 事件 B 的时间戳相同,则它们是并行独立的,即
C(A)=C(B)\Rightarrow A \nleftrightarrow B
大家可以参考下下面的图,加深下理解。

2020-05-01-PSQ3s2

Vector Clock

但是 Lamport Timestamp 不能很好的满足分布式系统,比如你不能区分两个事件是否有关联,或者在一个多点读的 key-value 数据库中,你无法确定保存哪一份副本(通常保存最新的那份副本)。

如下图所示,你其实无法单纯的通过 logic clock 比较来判断 Process 1 中的事件 3 和 Process 2 中的事件 8 是否有关系。

2020-05-01-w3C9Kq

如果事件 3 执行时间延迟几秒,这不会影响到事件 8。所以两个事件互不干涉,为了判断两个事件是否为这种情况,我们引入了 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。

如下图所示:

2020-05-01-8ndGfw

由此我们也可以知道,如果 A\rightarrow B ,那么 A 的 Vector Clock 中的每一个元素都会小于 B 中的每一个元素。

判断两个事件是否并行或独立

回到刚刚判断两个事件是不是有关联,我们可以看到 Process 1 中的 [3, 0, 0] 和 Process 2 中的 [2, 4, 2] ,其中 3 > 2 而 0 < 4,0 < 2 。有些 timestamp 大于对方,而有些 timestamp 又小于对方,由此我们可以得知这两个事件是互不相干的。

K/V 数据库中的应用

传统的分布式数据库存在一个 leader,leader 接收写入请求后,会将写入数据同步到集群一半以上的节点上面。一半以上节点反馈写入成功后,leader 再返回给客户端写入成功,有点类似于 Raft ,是单点写的。

如果我们引入了 Vector Clock,我们可以实现多点写,如 Dynamo 论文中所示。

假设 Key K 有三个副本 k1, k2, k3(目前是一样的),分别位于 M1, M2, M3 三台服务器上面,现在因为某种故障,导致了网络分区,三台机器均不能互相通信,但是每台机器仍然能够和客户端保持通讯。

其中 k1 副本被 client 1 持续更新,k2 副本被 client 2 持续更新。当三台机器之间互相通讯恢复的时候,进行副本同步时,应该保留哪个版本?如果只保留 k2,即采用 last write win 机制,那么同步后,client 1 会发现它写的数据丢了。

这个时候就需要 Vector Clock,更确切的说是 Version Clock(Dynamo 中是这么说的)。

为了处理这种场景,Dynamo 使用 Version Clock 来捕获同一份数据(Object) 的不同版本之间的因果关系(causality)。每个 Object 的每个版本会有一个相关联的 Version Clock , 形如 [(serverA, counter), (serverB, counter),...], 通过检查同一个 Object 不同版本的 Version Clock,可以决定是否可以完全丢弃一个版本,仅保留另外一个版本,还是需要将两个版本进行合并(merge)。如果 Object 的版本 A 的 Version Clock 中的每项 (server, counter) 在版本 B 的 Version Clock 中都有对应项,并且 counter 小于等于版本 B 中对应项的 counter,那么这个 Object 的版本 A 可以被丢弃,否则需要对两个版本进行 merge。

回到刚才的例子,k1 被更新,Version Clock (注:此处假设 k1/k2/k3 三个副本之前一模一样) 为[(M1, 1), (M2, 0), (M3, 0)],k2 被更新,Version Clock 为[(M1, 0), (M2,1), (M3, 0)],随后 k1 和 k2 网络通了,他们通过比较两个 Version Clock 发现两个 Version Clock 存在冲突,且不存在对方每一项 Logic Clock 小于自己的 Logic Clock,那么就两个版本都保留,当客户端来读 Key=K 的时候,两个版本的数据和对应的 Version Clock 都返回给客户端,由客户端进行冲突合并,客户端进行冲突合并后写入Key K的时候,带着合并后的 Version Clock [(M1, 1), (M2, 1), (M3, 0)] 发到M1 和 M2 两台机器,覆盖服务器版本,冲突解决。

Vector Clock 缺陷

系统伸缩(Scale)缺陷

其实 Vector Clock 对资源的伸缩支持并不是很好,因为对于一个 key 来说,随着服务器数量的增加,Vector Clock 中向量的元素也同样增长。假设集群有 1000 台机器,每次传递信息时都要携带这个长度为 1000 的 Vector,这个性能不太好接受。

非唯一缺陷

在正常的系统下面,假设所有的消息都是有序的(即同一台机器发送 消息 1 和 2 到另一台机器,另一台机器也会先接收到消息 1 再接收消息 2)。那么我们可以根据每一台机器的 Vector Clock 来恢复它们之间的计算(computation)关系,也就是每一种计算都有着对应自己独一无二的 Vector Clock。

但是,如果消息不是有序的,消息之间会‘超车‘(overtaking),那么问题就来了,看下图:

2020-05-02-uJrKNN

大家看看左右两张图,两种不一样的计算方式,但是最终 pq 上面产生了相同的 Vector Clock。也就是说,相同的 Vector Clock 并不代表唯一的 computation。

2020-05-02-ugBzFk

这张图中,我们可以看看 r 节点中的 j,我们无法判断这个 j 是从 p 那边的 e 传递过来还是 r 自己处理了一个事件,在自己 i 的基础上面 +1 。

解决办法 1

在 Vector Clock 中添加事件类型,例如用内部(internal),发送(send),接收(receive)3 种事件表明 Vector Clock。但是这样的话还是有问题,

2020-05-02-ajHByN

我们标明了 send 和 receive 两种事件,但是结果还是不同的 computation 产生了相同的 Vector Clock。

解决办法 2

将 Vector Clock 改为既包含接收到消息的时间和本地时间。例如下面这个图:

2020-05-02-ajHByN

将左边图改为 h:(<3,0>,<3,1>),i:(<1,0>,<3,2>,j:(<2,0>),<3,3>) ,右边图中变为 h:(<3,0>,<3,1>),i:(<2,0>,<3,2>,j:(<1,0>),<3,3>)

通过这种方法,我们能够确定每一种 computation 有着唯一的 Vector Clock。虽然这种会导致 Vector Clock 体积增长了一倍。

当然这种方法也不一定完全需要,因为只要我们能够保证消息发送到达的有序,即不产生消息超车(Overtaking)的情况下,原来的 Vector Clock 也够用了。

参考资料

https://en.wikipedia.org/wiki/Lamport_timestamps

https://jameshfisher.com/2017/02/12/what-are-lamport-timestamps/

https://towardsdatascience.com/understanding-lamport-timestamps-with-pythons-multiprocessing-library-12a6427881c6

https://www.zhihu.com/question/19994133

https://www.cnblogs.com/foxmailed/p/4985848.html

https://dl.acm.org/doi/10.1016/S0020-0190%2898%2900143-4

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342