逻辑时钟(Logical clock)

逻辑时钟是在假设没有全局时钟的情况下,通过允许给事件指定序列号(时间戳)来解决分布式系统中的事件顺序问题。在逻辑时钟范畴内使用以下概念来描述事件之间的关系:
happened-before关系aba→b 表示事件a于事件b前发生。 在同一进程内,事件的happened-before关系就是我们观察到的基于时间的事件发生先后顺序,在不同进程间的消息传递,消息发送事件先于消息接收事件发生。此外,happened-before关系满足传递性。通常情况下happened-before关系也称为因果序。
并发(concurrent): 两个事件之间不存在因果序(aba→b && bab→a),通常这两个事件发生在不同的进程并且尚未有消息传递。

Lamport时钟(Lamport Clocks)

在Lamport算法中,每个事件都有序列号(时间戳),每个报文都带有发送者的序列号。一条消息由发送消息事件和接收消息事件组成。序列号可以看作进程的逻辑时钟,每个消息携带的序列号可以看作消息发送者的逻辑时间戳。它是一个进程范围内的单调递增的计数器,每次事件发生前都会递增。当信息到达时,如果接收方的当前序列号小于或等于消息的序列号时会被设置为消息序列号 + 1。确保标记接收信息事件的序列号总是大于发送信息的序列号。

image.png

通过Lamport时钟,我们可以通过序列号来捕获事件的因果序,比如图中aba→b(进程内事件),fdf→d(进程间事件),dcd→c(传递性ded→eebe→bbcb→c)。可以看到事件a、d是并发的,因为不存在(ada→ddad→a)。

基于Lamport时钟,可以得到如果aba→b,那么事件b的序列号一定大于事件a的序列号。但是我们不能从事件b的序列号大于事件a的序列号推断出aba→b,例如图中事件i的序列号2大于a的序列号1,但是不存在aia→i。Lamport时钟对于因果关系是一种充分而不必要的关系,它保证了因果序的正确性,但不保证绝对时序的正确性。

向量时钟(Vector Clocks)

向量时钟不再是一个序列号,而是由所有进程的序列号组成的向量。通过向量时钟我们能够通过事件的逻辑时间戳推断出事件的因果序。为了更好说明问题,我们先假设分布式系统中进程是固定的,如上面例子中有三个进程P0, P1, P2,那么每个进程的向量时钟则是一个三元组V,其中每个项和进程的对应关系定义为(P0, P1, P2)。与Lamport时钟一样,每个消息会附带向量时钟,向量时钟的更新规则如下:

  1. 给事件添加时间戳前,自增进程对应的项的元素。比如例子中P0进程对V[0]进行自增

  2. 在给消息接收事件添加时间戳前,先更新本地进程的向量时钟(更新规则如下),再执行规则1

    for (i=0; i < num_elements; i++) 
        if (received[i] > system[i]) 
            system[i] = received[i];
    

通过向量时钟,我们就可以通过比较时间戳来确定事件间的happened-before关系。向量时钟规定:

  • 向量时钟时间戳v和w相等,当且仅当v和w中每个相应的元素都相等
  • 向量时钟时间戳v小于w,当前仅当v和w不相等,且v中的每个元素都小于或等于w中的相应元素

如果时间戳V的每个元素都小于或等于时间戳W的相应元素,则V对应的事件happened-before于W对应的事件。如果时间戳V的每个元素都大于或等于时间戳W的相应元素,则W对应的事件happened-before于V对应的事件。当既不存在v小于等于w,也不存在w小于等于v,则两个时间戳对应的事件是并发的。例如下图中的a和g是并发的。

image.png

前面我们假设组内进程是确定的,这样才能创建一个大小合适的向量。在实际执行中,并非所有进程都间都有消息传递,因此使用固定大向量是不必要的。可以用一组元组来代替向量,每个元组代表一个进程及其计数器,比如( { P0, 6 }, { P2, 2 } )。当进程发送消息时附带的时间戳为它所拥有的整个元集集合。当进程接收到一个消息并进行比较时,它会比较每个相关的元组对。例如,P0的值将与接收到的向量中包含P0的元组进行比较。如果缺少相应的进程ID,则以默认值0进行比较。