分布式系统中的时间、时钟和事件顺序 论文笔记
这篇笔记主要是用于记录阅读《Time, Clocks, and the Ordering of Events in a Distributed System》论文的要点以及我自己对于分布式系统时钟的理解。之前已经阅读过这篇文章几次了,每次阅读都对自己很有帮助。因此写下这篇论文笔记可以帮助自己进一步加深理解。
1978年Lamport在这篇论文中主要讨论的是分布式系统下的时钟和事件序问题,摒弃了物理时钟,提出了逻辑时钟的概念来解决分布式系统中区分事件发生的时序问题。
文章目录
分布式系统概念
分布式系统由不同的进程组成,不同进程可以经由消息通信。这篇论文说的分布式系统是从广义上来说的,一个互联网中的多计算机节点是分布式系统;单台机器的CPU、内存等内部组件也共同构成了一个分布式系统。事实上,如果相比组件内事件发生间隔而言,组件间的消息通信延迟不能忽略的话,这个系统就可以被认为是分布式系统。
物理时钟vs逻辑时钟
为什么分布式系统不适用物理时钟(physical clock)记录事件?每个事件对应打上一个物理时钟时间戳,当需要比较顺序的时候比较相应时间戳即可。这是因为在现实世界中物理时钟有世界统一的标准,但是在分布式系统中每个计算机节点记录的时间是不一样的,即使通过设置NTP时间同步,各个节点间也存在毫秒级的偏差。每个节点的CPU都有自己的计时器,而不同计时器之间会产生时间偏移,最终导致不同节点上面的时间不一致。假设如果A节点的物理时钟比B节点的要快1分钟,那么即使B先发出的消息,A的消息在后面1s发出,那么在整个分布式系统中A的消息也会被认为比B节点消息先发出。但是在分布式系统中,NTP时间同步算法就是常用来同步不同节点的物理时钟。但是这种方式也会产生误差,这种级别的误差在金融分布式事务的场景下是不能接受的。
因此当我们在考虑对一个时钟系统的正确性时,我们不能将定义的正确性基于物理时钟之上,因为这需要引入持有物理时钟标准。因而分布式系统需要有另外的方法记录事件顺序关系,这就是逻辑时钟(logical clock)。
事件序列
论文的核心在于讨论事件序列。事件序列指的是特定环境中的事件发生的先后顺序。时间只不过是事件序列的一种定义方法。即使是通过时间来定义,也存在物理时间、逻辑时间等方法。
一个分布式系统由众多独立的节点组成,称为Process。每个节点的事件来源有两类:
- 内部事件:此类事件由节点内部产生,可以认为与其他节点不产生关联;
- 外部事件:此类事件由其他节点个体刺激本节点个体产生,这种外部事件的表现形式通常为消息。
事件序列分为两种:偏序事件序列和全序事件序列。
在数学上,顺序又是如何描述的呢?
我们先看一下序理论中的两种序关系:偏序(partial ordering)和全序(total ordering)。
偏序
偏序的数学定义:
假设≤是集合S上的一个二元关系,如果≤满足:
- 自反性:对于S中任意的元素a,都有a≤a;
- 反对称性:如果对于S中的两个元素a和b,a≤b且b≤a,那么a=b;
- 传递性:如果对于S中的三个元素,有a≤b且b≤c,那么a≤c。
上面的内容可以先不用理解,关键点在于:偏序关系是一种序关系,但只是部分元素有序,并不是全部都可以比较。这里的部分元素其实是有因果关系的事件。
例如,发生在个体PA的一个事件E1,该事件的影响是产生了消息M,M传递至个体B,进而导致事件E2的发生,即说明E1->E2:E1导致了E2的产生。而对于两个相互独立的事件,我们无法从原理上判断事件先后顺序。
在分布式系统中,一个进程process内的多个事件,自然地具备事件的先后顺序,或者说一个process本身就是一个有先验顺序的全序的事件集合。除了process内在的顺序,消息的发送和接收事件也是有因果序的。基于这两点,我们定义happen-before关系,写作->。
happen-before关系->,是一个分布式系统中事件集合上满足如下条件的最小关系:
- 在同一个process内部,如果事件a早于事件b发生,称为a->b;
- 如果a和b是两个不同process的事件,且b是a发送的消息的接收者,同样a->b;
- 满足传递性,如果a->b,b->c,那么a->c。
如果a↛b∧b↛a,那么称事件ab是并发的。
Lamport在论文中提出了一种利用逻辑时钟设计的一种偏序系统方法:
- 每个process存在独立的事件序列发生器,每次产生新的事件,该序列发生器自增1,并将结果赋予该事件;
- 如果process的事件E需要向其他process发送消息M,那么在M中携带E的序列号;
- 如果Process收到外部消息M,获取M中携带的序列号,与自身的事件序列发生器比较并取最大值,然后自增1,赋给由于M而触发的新的外部事件。
根据上面的定义,我们可以得到如下结论:
- process内部的事件均可以比较先后顺序;
- process之间的因果事件可以确定先后顺序,而process之间的独立事件则无法比较。
在图中,a->b意味着从a可以沿着线到b,例如p1->r4。因为有:p1->q2,q2->q3,q3->q4,q4->r3,r3->r4。而p3->q3不成立。这个定义的另外一种理解:a->b意味着从a和b有因果序,例如p3和q3是并发的,意味着我们不能推断两者的时序。
全序
全序比偏序的要求更为严格一些,在偏序的基础上,多了一个完全性的条件:
- 完全性:对于S中的任意a、b元素,必然有a≤b或b≤a。
实际上,全序是在偏序的基础上,要求集合内全部元素都必须可以比较。
总体来说,偏序是部分可比较的序关系,全序是全部可比较的序关系。
全序与偏序的比较
我们可以通过简单的有向图来描述全序和偏序的不同:
上图中S1上的图示关系,描述的是整数之间的大小顺序,是一种全序关系,可以看到任意两个元素之间可以比较顺序。而S2上的关系,描述的是集合之间的包含关系,是一种偏序关系,其中v2和v3是不可比较的。
逻辑时钟Logical Clocks
逻辑时钟定义
Leslie Lamport在1978年提出逻辑时钟的概念,并描述了一种逻辑时钟的表示方法,这个方法被称为Lamport时间戳(Lamport timestamps)。概念上,时钟即给事件分配序号的方式。
分布式系统按是否存在节点交互可分为三类事件,一类发生于节点内部(下图中的红色标记的事件),二是发送事件(下图中的蓝色标记事件),三是接收事件(下图中的黑灰色标记事件)。如下图所示:
定义Ci为进程Pi的时钟,Ci为进程Pi的事件a的序号。不关心哪个进程时,Ci记为C。该时钟与物理时钟physical time没有关系。
我们应该基于事件的顺序来定义正确性,禁止引入物理时钟来定义逻辑时钟的正确性。如果两个事件有happen-before关系,时钟序号应该与关系一致,即Clock Condition。
Clock Condition:任意事件a,b,如果a->b,那么C≤C。
该命题的逆命题不成立。
根据之前happen-before关系的定义,显然只要如下的两个条件C1、C2成立,Clock Condition则成立:
C1.如果a,b是Pi的事件,且a在b之前发生,则定义Ci≤Ci。
C2.如果a是Pi发送消息事件,b是Pj接收消息事件,定义Ci≤Cj。
如何保证C1和C2这两个条件成立呢?只需要根据实现规则implement rule IR1和IR2即可保证C1和C2.
IR1.Pi的两个连续事件之间要递增该process的Ci。
IR2.a是Pi发送消息事件,那么消息m中携带一个时间戳Tm=Ci。Pj接收到消息m之后,Pj需要将Cj设置为等于或大于当前值,且大于Tm的值。
从时空图的角度考虑时钟,我们假设进程的时钟在每个事件之间tick,且每次tick增加1。比如a、b是Pi进程中的事件,如果Ci=4,C1=7,那么这两个事件之间时钟会走过5、6、7。如果我们通过时间线将所有tick的点连接起来,那么Fig1中的时空图会变成这样:
tick line实际上就是时空笛卡尔坐标系的时间轴,将时间线拉直就能得到下图Fig3。
逻辑时钟原理
- 每个事件对应一个Lamport时间戳,初始值为0.
- 如果事件在节点内发生,时间戳加1.
- 如果事件属于发送事件,时间戳加1并在消息中带上该时间戳.
- 如果事件属于接收事件,时间戳=Max(进程本地时间戳,消息中的时间戳)+1.
假设有事件a、b,C(a)、C(b)分别表示事件a、b对应的Lamport时间戳,如果a->b,则C(a)<C(b),a发生在b之前,例如上图中所示,C1->B1,那么C(C1)<C(B1)。通过该定义,事件集中Lamport时间戳不等的事件可以进行比较,我们获得事件的偏序关系。
如果C(a)=C(b),那么a、b事件的顺序是如何判定的?首先能够确定的是,当C(a)=C(b)时,这两者之间肯定不是因果关系,所以它们之间的先后顺序并不会影响结果,我们这里只需要给出一种确定的方式来定义它们之间的先后顺序就能够扩展得到全序关系。假设a、b分别在节点P、Q上发生,Pi、Qj分别表示给我们P、Q的进程编号,如果C(a)=C(b)并且Pi<Qj,同样定义为a发生在b之前,记作a=>b(全序关系)。假如我们对图1的A、B、C分别编号Ai=1、Bj=2、Ck=3,因C(B4)=C(C3)并且Bj<Ck,则B4=>C3。
通过以上定义,我们可以对所有事件排序,获得事件的全序关系。按照上图中的因果顺序以及判定方法,事件排序为:C1=>B1=>B2=>A1=>B3=>A2=>C2=>B4=>C3=>A3=>B5=>C4=>C5=>A4。其中B5在时间轴上显示比A3先发生,但是我们根据判定方法,全序关系为A3=>B5。
用全序关系解决分布式互斥问题(分布式锁资源抢占)
问题描述:单机多进程程序可由锁进行同步,是因为这些进程都运行在操作系统上,有center为每个进程的资源请求排序,这个center知道所有需要进行同步的进程的所有信息。但是在分布式系统中,各个进程运行在各自的主机上,没有center的概念,那么分布式系统中多进程应该如何进行同步?也就是分布式锁应该如何实现?多个进程共享一个资源。要求同一时间只有一个进程能够使用该资源,其实这就是分布式锁的问题。Lamport的论文中提出了解决这个问题的算法需要满足下面三个条件:
- A process which has been granted the resource must release it before it can be granted to another process;(已经获得资源的进程先释放,其他进程才能够获得资源授权)
- Different requests for the resource must be granted in the order in which they are made;(资源授予顺序需要按照申请的顺序)
- If every process which is granted the resource eventually releases it,then every request is eventually granted。(如果已经获得资源的进程释放了资源,其他想申请的进程最终能够获得资源)
为了简化问题,我们做如下假设:
- 任何两个进程Pi,Pj,它们之间接收到的消息的顺序与发送消息的顺序一致,并且每个消息一定能够被接收到。
- 每个进程都维护一个不被其他进程所知的请求队列。并且请求队列初始化为包含一个T0:P0资源请求,P0用于该共享资源,T0是初始值小于任何时钟值
算法如下:
- To request the resource,process Pi sends the message Tm:Pi requests resource to every pther process,and puts that message on its request queue,where Tm is the timestamp of the message。为请求该项资源,进程Pi发送一个(Tm:Pi)资源请求(请求锁)消息给其他所有进程,并将该消息放入自己的请求队列,在这里Tm代表了消息的时间戳。
- When process Pj receives the message Tm:Pi requests resource,it places it on its request queue and sends a (timestamped) acknowledgment message to Pi。当进程Pj收到(Tm:Pi)资源请求消息后,将它放到自己的请求队列中,并发送一个带时间戳的确认消息给Pi。
- To release the resource,process Pi removes any Tm:Pi requests resource message from its request queue and sends a (timestamped) Pi releases resource message to every other process。释放该项资源(释放锁)时,进程Pi从自己的消息队列中删除所有的(Tm:Pi)资源请求,同时给其他所有进程发送一个带有时间戳的Pi资源释放消息。
- When process Pj received a Pi releases resource message,it removes any Tm:Pi requests resource message from its request queue。当进程Pj收到Pi资源释放消息后,它就从自己的消息队列中删除所有的(Tm:Pi)资源请求。
- Process Pi granted the resource when the following two conditions are satisfied:当同时满足如下两个条件时,就将资源分配(锁占用)给进程Pi:
- There is a
Tm:Pi
requests resource message in its request queue which is ordered before any other request in its queue by the relation⇒
。按照全序关系排序后,(Tm:Pi)资源请求排在它的请求队列的最前面 Pi
has received a message from every other process timestamped later thanTm
。Pi已经从所有其他进程都收到了时间戳>Tm的消息
下面我们通过一个示例说明上面的获取资源算法过程,假设我们有3个进程,根据算法说明,初始化状态各个进程请求队列里面都是(0:0)状态,此时锁属于P0.
接下来P1会发出请求资源的消息给所有其他进程P0,P2,并且放到自己的请求队列里面,根据逻辑时钟算法,P1的时间戳加1,而接收消息的P0和P2的逻辑时钟时间戳为消息时间戳+1=2.
收到P1的请求消息之后,P0和P2要发送确认消息给P1表示自己收到消息了。由于目前请求队列里面第一个不是P1发出的请求,所以此时锁仍然属于P0。但是由于收到了确认消息,此时P1已经满足了获取资源的其中一个条件:P1已经收到了来自其他所有进程时间戳大于1的消息。
假设P0此时释放了锁,发送释放资源的消息给P1和P2,P1和P2收到消息之后把请求(0:0)从队列里面删除。
当P0释放了资源之后,我们发现P1满足了获取资源的两个条件:它的资源请求(1:1)在队列最前面;P1已经收到了其他所有进程时间戳大于1的回应消息。此时P1获取到了锁。
其实关键思想不复杂,既然分布式系统中没有center的概念,那我请求共享资源时我就让其他所有进程都知道我要请求该资源,拥有资源的进程释放资源时也告诉所有进程,我要释放该资源,想请求该资源的你们可以按序(逻辑时钟的作用,这里重点说明逻辑时钟不能保证在绝对物理时间上请求的排序)请求了。这样每个进程都知道其他进程的状态,和center的作用是一样的。
对于分布式锁问题,多个请求不一定是按照绝对物理时钟排序才可以,只要我们有这样一个算法,这个算法可以保证多个进程的请求按照这个算法总能得到同一个排序,就可以了。而按照绝对物理时钟排序只是其中一个可行的算法。
但是我们需要特别注意,这种算法并不是容错的,它要求所有进程都非常可靠,一旦一个进程挂了或者出现网络分区的情况,是无法工作的,同时对我们提出的网络要求也非常严格,要求发出的消息一定被接收到,这个在生产系统中是很难做到的。所以这是一个理想状况下的算法实现,达不到一个工业级的算法要求。但是对于分布式系统来说,仍然是非常有意义的,Lamport提出的逻辑时钟概念可以说是分布式一致性算法的开山鼻祖,后续的所有分布式算法都有它的影子。逻辑时钟定义了分布式系统里面的时间概念,解决了分布式系统中区分事件发生的时序问题。
来源:https://blog.csdn.net/JKerving/article/details/102723101