随着数据量的上升,传统单机架构存在的瓶颈已不能满足对性能和容量的要求,从而分布式系统变得越来越火热,但另一方面, 分布式也带来了很多相对于单机架构不同的问题。其中一个问题就是多节点的时间同步问题:不同节点上的物理时钟难以同步,导致无法区分在分布式系统中多个节点的事件顺序。
早在1978年,Lamport在《Time, Clocks and the Ordering of Events in a Distributed System》中,就提出了逻辑时钟的概念,就是用来解决分布式系统中事件发生的顺序问题。
事件的因果
“Time is an illusion.”爱因斯坦如是说。
逻辑时钟是Lamport在1978年提出的一种在分布式系统中对事件进行时间戳排序的方法,在其中定义了因果关系,称为before。
例如,before在航班满员,航班可预订。这里“事件预订”before“航班满员”,预订和满员就形成了因果关系。
现实世界中,确定事件预订发生在事件满员之前,需要预订发生在比满员更早的时间。 因果关系是一个事件(因)和第二个事件(果)之间的作用关系, 其中后一个事件被认为是前一个事件的结果。 一般来说,一个事件是很多原因综合产生的结果, 而且原因都发生在较早的时间点。
而在分布式系统中,有时不可能说两个事件中的一个首先发生。 关系“happened before”只是系统中事件的部分排序。
部分排序
在分布式系统中,事件A发生在事件B之前,如果A发生在比B更早的时间,需要系统正确的满足规范。 如果分布式系统以物理时间为单位,可能存在时钟不完全准确,没办法保持精确的物理时间的问题。 因此,在逻辑时钟论文里,定义了“before”关系,而不使用物理时钟。
“before”标记为=>,需要满足三个条件:
如果a和b是同一进程中发生的事件,且a先于b,则a->b
如果a是一个进程中的发送消息,b是另一个进程中接收此消息,那么a->b
如果a->b并且b->c,那么a->c。两个事件是并发的,如果a≠>b和b≠>a
对于任意事件a,定义a≠>a。
另一种说法是a->b意味着事件a是事件b的因。两个事件并发说明二者不能发生因果影响, 两个事件相互独立。这就意味着部分排序。
这样,在分布式系统中,事件的顺序可以根据发送的消息来定义,只考虑实际发送的消息。
逻辑时钟
由部分排序可知,问题的关键点在于节点间的交互要在事件发生顺序上达成一致, 而不是对于时间达成一致。所以,逻辑时钟指的是分布式系统中用于区分时间发生顺序的机制。 从某种意义上来讲,现实世界的物理时间其实是逻辑时钟的特例。
分布式系统中,按是否存在节点交互可分为三类事件,节点内部,发送事件,接收事件。
Lamport逻辑时钟的原理如下:
每个事件对应一个Lamport时间戳,初始值为0.
如果事件发生在节点内部,本地进程中的时间戳加1.
如果事件是发送事件,本地进程中的时间戳加1并在消息中带上该时间戳
如果事件属于接收事件,本地进程中的时间戳 = Max(本地时间戳,消息中的时间戳) + 1
下面详细说明逻辑时钟的原理。
单机多进程程序可以由锁进行同步,因为这些进程都运行在操作系统上, 可以由操作系统为它们进行排序,操作系统知道所有需要同步进程的所有信息。 但是在分布式系统中,各个进程运行在各个节点上, 那么分布式系统中多进程怎么进行同步呢?
把逻辑时钟引入到系统中,由一个抽象观点开始,在这个观点中时钟只是给事件分配数字的一种方式, 其中这个数字就是事件发生的时间。给每个进程Pi分配时钟Ci,时钟Ci是一个函数,负责分配数字, 例如为事件a分配时钟为Ci(a)。系统时间函数C为任意事件b分配时间C(b)=Cj(b),如果b是发生在进程Pj下。 这里的时钟是逻辑时钟不是物理时钟,C函数可以是与物理时钟无关的计数器实现。
这里由于论文时间较早,所以都是以线程为例,可以推广到分布式节点。
逻辑时钟的定义必须基于事件发生的顺序而不是物理时钟。如果事件a发生在事件b之前, 那么a应该发生在b的更早时间。表述如下:如果a->b,那么C(a)
logic_clock1
如图1,这里p1,p2,p3都与q3并发,这意味着它们都和q3同一时间发生,但这与时间条件相矛盾, 因为p2->p3。
对关系的定义中很容易看出,如果以下两个条件成立,则时钟条件是满足的。
• 条件1,如果a和b都是进程Pi中的事件,并且a->b,那么Ci(a)
• 条件2,如果a是Pi发送一个消息的事件,b是Pj中接收这个消息的事件,那么Ci(a)
考虑时空图,假设一个进程时钟ticks分配数字,发生在进程事件之间。 例如a和b是Pi的连续事件,Ci(a)=4,Ci(b)=7,时钟心跳5,6,7发生在两个事件之间。 这样可以在不同进程的所有相似的ticks之间画一条虚线形成图1空间图,如图2。
logic_clock1
条件1意味着同一进程上的任意事件之间必须要虚线,条件2意味着图中每个消息线都要跨越虚线。 从图中可以看出为什么这两个条件是时钟条件。
进程Pi的时钟用Ci表示,Ci(a)表示事件a的时钟。事件之间Ci的值会变化,改变Ci本身并不构成事件。
为了确保满足时钟条件1,2,需要遵守以下实现规则:
• IR1. 每个进程Pi在两个连续事件之间递增Ci.
• IR2. (a)如果a是进程Pi发送消息m的事件,则消息m包含时间戳Tm=Ci(a)
• IR2. (b)在收到消息m时,处理Pj设置Cj大于或等于现在Tm的值
进行全局事件排序,可以使用满足时钟条件的时钟系统在所有系统事件的集合上进行总排序。
能够对事件进行完全排序,对于实现分布式系统非常有用。 实现一个正确的逻辑时钟系统就是为了获得这样的总排序。 考虑共享资源的进程或节点固定的集合组成的系统,一次只能有一个进程或者节点使用资源, 因此进程必须同步自己,以避免发生冲突。
论文提到解决这一问题要满足三个条件:
Ⅰ 已被授予资源的进程必须先将其释放,然后才能将其授予另一个进程。
Ⅱ 对资源的不同请求必须按请求的顺序授予。
Ⅲ 如果每个被授予资源的进程最终都会释放,那么每个请求最终都会被接受。
为解决这些问题,并实现一个满足条件的时钟系统,满足规则IR1和IR2。 定义一个=>用来给全部事件排序。有了这个顺序,找到一个解决方案就变得简单了, 它只涉及确保每个进程或节点知道所有其他进程或节点的操作了。
为了简化问题,进行一些不是必须的假设,这些假设的引入是为了避免分散实现细节。 假设对于两个进程Pi和Pj,从Pi发送到Pj的消息时以相同的顺序接收的。 此外,我们假设每个消息最终都被接收。并且假设一个进程可以直接向每个其他进程发送消息。
每个进程都维护自己的请求队列,这是任何其他进程都看不到的。 假设请求队列最初包含单个消息T0:P0请求资源,其中P0是最初授予资源的进程, T0小于任何时钟的初始值。
通过五条规则定义算法,为了方便起见,假设每个规则定义的操作形成单个事件。
要请求资源,进程Pi发送消息Tm:Pi请求给每个其他进程,并将该消息放到其请求队列中, 其中Tm是消息的时间戳。
每个进程Pj接收请求资源消息Tm:Pi,它将其放置在请求队列中,并向Pi发送一个带有时间戳的确认消息。
为了释放资源,进程Pi从其请求队列中删除Tm:Pi请求西苑消息,并且发送Pi时间戳释放资源消息到其他进程。
当进程Pj接收一个Pi释放资源消息时,它从其请求队列中删除任一Tm:Pi请求资源。
当满足以下两个条件时,进程Pi被赋予资源:(1)在其队列中由一个Tm:Pi消息,它在队列中 =>其他请求。(为了定义=>,我们定义一个消息,使用发送消息的事件来识别。)
(2)Pi收到比Tm更晚的每一个其他进程的消息。
可以很容易的看到算法满足条件Ⅰ-Ⅲ。
举例说明,假设有3个进程,根据算法说明,初始化状态各个进程队列里面都是(0,0)状态, 此时锁属于P0。
logic_clock3
接着P1会发送请求资源的消息给所有其他进程,并且放到自己的请求队列里面, 根据算法,P1的时钟计数变为1,而接受消息的P0和P2的时钟为消息时间戳+1。
logic_clock4
收到P1的请求之后,P0和P2要发送确认消息给P1表示自己已经收到了。 由于目前请求队列里面第一个不是P1发出的请求,所以此时锁仍属于P0。 但是由于收到了确认消息,此时P1已经满足了获取资源的第一个条件: P1已经收到了其他所有进程时间戳大于1的消息。
logic_clock5
假设P0此时释放了锁,发送释放资源的消息给P1和P2,P1和P2收到消息后把请求(0,0) 从队列中删除。
logic_clock6
当P0释放了资源之后,P1满足了获取资源的两个条件,它的请求在队列最前面和P1 已经收到了其他所有进程时间戳大于1的消息。也就是此时P1获得了锁。
总结
在逻辑时钟的论文中描述的算法大致就像上面描述的一样,但显而易见,这个算法并不是容错的,只要有一个进程挂了或者响应很慢,都会影响到整个分布式系统。
但逻辑时钟的思路就如上面描述的那样,是整个分布式一致性算法的基石, 所有的分布式一致性算法都有逻辑时钟的影子在内。 逻辑时钟定义了分布式系统里面的时间概念,解决了分布式系统中区分时间发生的时序问题。
实际上后续产生的矢量时钟、全局时钟亦或者混合时钟,都是对逻辑时钟的增强, 其基础都是逻辑时钟。
以上为事件的因果和逻辑时钟,「分布式技术专题」是国产数据库hubble团队精心整编,专题会持续更新,欢迎大家保持关注。