分布式系统:Lamport 逻辑时钟

分布式系统解决了传统单体架构的单点问题和性能容量问题,另外一方面也带来了不少的问题,其中一个问题就是多节点的时间同步问题:不一样机器上的物理时钟难以同步,致使没法区分在分布式系统中多个节点的事件时序。1978年Lamport在《Time, Clocks and the Ordering of Events in a Distributed System》中提出了逻辑时钟的概念,来解决分布式系统中区分事件发生的时序问题。html

什么是逻辑时钟

逻辑时钟是为了区分现实中的物理时钟提出来的概念,通常状况下咱们提到的时间都是指物理时间,但实际上不少应用中,只要全部机器有相同的时间就够了,这个时间不必定要跟实际时间相同。更进一步,若是两个节点之间不进行交互,那么它们的时间甚至都不须要同步。所以问题的关键点在于节点间的交互要在事件的发生顺序上达成一致,而不是对于时间达成一致。算法

综上,逻辑时钟指的是分布式系统中用于区分事件的发生顺序的时间机制。 从某种意义上讲,现实世界中的物理时间实际上是逻辑时钟的特例。网络

为何须要逻辑时钟

时间是在现实生活中是很重要的概念,有了时间咱们就能比较事情发生的前后顺序。若是是单个计算机内执行的事务,因为它们共享一个计时器,因此可以很容易经过时间戳来区分前后。同理在分布式系统中也经过时间戳的方式来区分前后行不行?架构

答案是NO,由于在分布式系统中的不一样节点间保持它们的时钟一致是一件不容易的事情。由于每一个节点的CPU都有本身的计时器,而不一样计时器之间会产生时间偏移,最终致使不一样节点上面的时间不一致。也就是说若是A节点的时钟走的比B节点的要快1分钟,那么即便B先发出的消息(附带B的时间戳),A的消息(附带A的时间戳)在后一秒发出,A的消息也会被认为先于B发生。并发

那么是否能够经过某种方式来同步不一样节点的物理时钟呢?答案是有的,NTP就是经常使用的时间同步算法,可是即便经过算法进行同步,总会有偏差,这种偏差在某些场景下(金融分布式事务)是不能接受的。app

所以,Lamport提出逻辑时钟就是为了解决分布式系统中的时序问题,即如何定义a在b以前发生。 值得注意的是,并非说分布式系统只能用逻辑时钟来解决这个问题,若是之后有某种技术可以让不一样节点的时钟彻底保持一致,那么使用物理时钟来区分前后是一个更简单有效的方式。分布式

如何实现逻辑时钟

时序关系与相对论

经过前面的讨论咱们知道经过物理时钟(即绝对参考系)来区分前后顺序的前提是全部节点的时钟彻底同步,但目前并不现实。所以,在没有绝对参考系的状况下,在一个分布式系统中,你没法判断事件A是否发生在事件B以前,除非A和B存在某种依赖关系,即分布式系统中的事件仅仅是部分有序的。性能

上面的结论跟狭义相对论有殊途同归之妙,在狭义相对论中,不一样观察者在同一参考系中观察到的事件前后顺序是一致的,可是在不一样的观察者在不一样的参考系中对两个事件谁先发生可能具备不一样的见解。当且仅当事件A是由事件B引发的时候,事件A和B之间才存在一个前后关系。两个事件能够创建因果关系的前提是:两个事件之间能够用等于或小于光速的速度传递信息。 值得注意的是这里的因果关系指的是时序关系,即时间的先后,并非逻辑上的缘由和结果。cdn

那么是否咱们能够参考狭义相对论来定义分布式系统中两个事件的时序呢?在分布式系统中,网络是不可靠的,因此咱们去掉能够速度的约束,能够获得两个事件能够创建因果(时序)关系的前提是:两个事件之间是否发生过信息传递。 在分布式系统中,进程间通讯的手段(共享内存、消息发送等)都属于信息传递,若是两个进程间没有任何交互,实际上他们之间内部事件的时序也可有可无。可是有交互的状况下,特别是多个节点的要保持同一副本的状况下,事件的时序很是重要。htm

Lamport 逻辑时钟

分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。注意:如下文章中说起的时间戳如无特别说明,都指的是Lamport 逻辑时钟的时间戳,不是物理时钟的时间戳

逻辑时钟定义

Clock Condition.对于任意事件a, b:若是a \to b\to表示a先于b发生),那么C(a)< C(b), 反之否则, 由于有多是并发事件 C1.若是ab都是进程P_i里的事件,而且ab以前,那么C_i(a) < C_i(b) C2.若是a是进程P_i里关于某消息的发送事件,b是另外一进程P_j里关于该消息的接收事件,那么C_i(a) < C_j(b)

Lamport 逻辑时钟原理以下:

图1

  1. 每一个事件对应一个Lamport时间戳,初始值为0
  2. 若是事件在节点内发生,本地进程中的时间戳加1
  3. 若是事件属于发送事件,本地进程中的时间戳加1并在消息中带上该时间戳
  4. 若是事件属于接收事件,本地进程中的时间戳 = Max(本地时间戳,消息中的时间戳) + 1

假设有事件a、b,C(a)、C(b)分别表示事件a、b对应的Lamport时间戳,若是a发生在b以前(happened before),记做 a \to b,则有C(a) < C(b),例如图1中有 C1 \to B1,那么 C(C1) < C(B1)。经过该定义,事件集中Lamport时间戳不等的事件可进行比较,咱们得到事件的偏序关系(partial order)。注意:若是C(a) < C(b),并不能说明a \to b,也就是说C(a) < C(b)a \to b的必要不充分条件

若是C(a) = C(b),那a、b事件的顺序又是怎样的?值得注意的是当C(a) = C(b)的时候,它们确定不是因果关系,因此它们之间的前后其实并不会影响结果,咱们这里只须要给出一种肯定的方式来定义它们之间的前后就能获得全序关系。注意:Lamport逻辑时钟只保证因果关系(偏序)的正确性,不保证绝对时序的正确性。

一种可行的方式是利用给进程编号,利用进程编号的大小来排序。假设a、b分别在节点P、Q上发生,P_i、Q_j分别表示咱们给P、Q的编号,若是 C(a) = C(b) 而且 P_i < Q_j,一样定义为a发生在b以前,记做 a \Rightarrow b(全序关系)。假如咱们对图1的A、B、C分别编号A_i = 一、B_j = 二、C_k = 3,因 C(B4) = C(C3) 而且 B_j < C_k,则 B4 \Rightarrow C3

经过以上定义,咱们能够对全部事件排序,得到事件的全序关系(total order)。上图例子,咱们能够进行排序:C1 \Rightarrow B1 \Rightarrow B2 \Rightarrow A1 \Rightarrow B3 \Rightarrow A2 \Rightarrow C2 \Rightarrow B4 \Rightarrow C3 \Rightarrow A3 \Rightarrow B5 \Rightarrow C4 \Rightarrow C5 \Rightarrow A4

观察上面的全序关系你能够发现,从时间轴来看B5是早于A3发生的,可是在全序关系里面咱们根据上面的定义给出的倒是A3早于B5,能够发现Lamport逻辑时钟是一个正确的算法,即有因果关系的事件时序不会错,但并非一个公平的算法,即没有因果关系的事件时序不必定符合实际状况。

如何使用逻辑时钟解决分布式锁问题

上面的分析过于理论,下面咱们来尝试使用逻辑时钟来解决分布式锁问题。

分布式锁问题本质上是对于共享资源的抢占问题,咱们先对问题进行定义:

  1. 已经得到资源受权的进程,必须在资源分配给其余进程以前释放掉它;
  2. 资源请求必须按照请求发生的顺序进行受权;
  3. 在得到资源受权的全部进程最终释放资源后,全部的资源请求必须都已经被受权了。

首先咱们假设,对于任意的两个进程P_iP_j,它们之间传递的消息是按照发送顺序被接收到的, 而且全部的消息最终都会被接收到。 每一个进程会维护一个它本身的对其余全部进程都不可见的请求队列。咱们假设该请求队列初始时刻只有一个消息(T_0:P_0)资源请求,P_0表明初始时刻得到资源受权的那个进程,T_0小于任意时钟初始值

  1. 为请求该项资源,进程P_i发送一个(T_m:P_i)资源请求(请求锁)消息给其余全部进程,并将该消息放入本身的请求队列,在这里T_m表明了消息的时间戳
  2. 当进程P_j收到(T_m:P_i)资源请求消息后,将它放到本身的请求队列中,并发送一个带时间戳的确认消息给P_i。(注:若是P_j已经发送了一个时间戳大于T_m的消息,那就能够不发送)
  3. 释放该项资源(释放锁)时,进程P_i从本身的消息队列中删除全部的(T_m:P_i)资源请求,同时给其余全部进程发送一个带有时间戳的P_i资源释放消息
  4. 当进程P_j收到P_i资源释放消息后,它就从本身的消息队列中删除全部的(T_m:P_i)资源请求
  5. 当同时知足以下两个条件时,就将资源分配(锁占用)给进程P_i
    • 按照全序关系排序后,(T_m:P_i)资源请求排在它的请求队列的最前面
    • i已经从全部其余进程都收到了时间戳>T_m的消息、

下面我会用图例来讲明上面算法运做的过程,假设咱们有3个进程,根据算法说明,初始化状态各个进程队列里面都是(0:0)状态,此时锁属于P0。

初始状态

接下来P1会发出请求资源的消息给全部其余进程,而且放到本身的请求队列里面,根据逻辑时钟算法,P1的时钟走到1,而接受消息的P0和P2的时钟为消息时间戳+1。

请求资源

收到P1的请求以后,P0和P2要发送确认消息给P1表示本身收到了。注意,因为目前请求队列里面第一个不是P1发出的请求,因此此时锁仍属于P0。可是因为收到了确认消息,此时P1已经知足了获取资源的第一个条件:P1已经收到了其余全部进程时间戳大于1的消息。

返回确认

假设P0此时释放了锁(这里为了方便演示作了这个假设,实际上P0何时释放资源均可以,算法都是正确的,读者可自行推导),发送释放资源的消息给P1和P2,P1和P2收到消息以后把请求(0:0)从队列里面删除。

释放资源

当P0释放了资源以后,咱们发现P1知足了获取资源的两个条件:它的请求在队列最前面;P1已经收到了其余全部进程时间戳大于1的消息。 也就是说此时P1就获取到了锁。

值得注意的是,这个算法并非容错的,有一个进程挂了整个系统就挂了,由于须要等待全部其余进程的响应,同时对网络的要求也很高。

总结

若是你以前看过2PC,Paxos之类的算法,相信你看到最后必定会有一种似曾相识的感受。实际上,Lamport提出的逻辑时钟能够说是分布式一致性算法的开山鼻祖,后续的全部分布式算法都有它的影子。咱们不能想象现实世界中没有时间,而逻辑时钟定义了分布式系统里面的时间概念,解决了分布式系统中区分事件发生的时序问题。

参考资料

相关文章
相关标签/搜索