分布式系统-向量时钟(Vector Clock)
Leslie Lamport的论文中《Time, Clocks and the Ordering of Events in a Distributed System》首次提出了逻辑时钟的概念,解决了分布式系统的事件全序关系,但是对于同时发生的事件无法定义,导致无法描述事件的因果关系。而Vector Clock是在Lamport时间戳基础上演进的另一种逻辑时钟方法,它通过vector结构不但记录本节点的Lamport时间戳,同时也记录了其他节点的Lamport时间戳,因此能够很好描述同时发生关系以及事件的因果关系。当系统有N个进程时,则存在N个逻辑时钟向量,一个进程对应一个时钟。
为什么需要向量时钟
我下面会举两个例子对这个问题进行说明,不过我们需要先回顾一下Lamport逻辑时钟算法,它提供了一种判断分布式系统中事件全序关系的方法:如果 a -> b,那么 C(a) < C(b),但是 C(a) < C(b) 并不能说明 a -> b。也就是说**C(a) < C(b) 是 a -> b 的必要不充分条件,我们不能通过 Lamport 时间戳对事件 a、b 的因果关系进行判断。**以下图为例
假设有三个进程在发消息,Ts(mi)表示消息mi的发送时间戳,Tr(mi)表示消息mi的接受时间戳,显然有Ts(mi)<Tr(mi),但是这个逻辑时钟无法直接对相应事件进行因果关系判断。
我们可以发现在进程P2中,Tr(m1)<Ts(m3),说明m3是在m1被接收之后发送的,也就是说m3的发送跟m1的接收有关系。但是我们可以观察Tr(m1)<Ts(m2),但实际上m2的发送和m1的接收没有因果关系。
再举一个例子:
在使用分布式数据库的时候,不同节点数据的一致性一向是一个经典且难以解决的问题,而这个问题的根源是难以实现一个全局统一的时钟。比如:
如上图所示,A,B,C表示分布式系统中的三个数据库,纵轴表示时间。在 TA1 时刻A做出了更改Key=Value1,这次更改消息在TC2 时刻传输到了C;在 TB1 时刻 B做出了更改Key=Value2,这次更改消息在 TC1 时刻传输到C。那么问题就在于C数据库中的Key应该等于Value1还是Value2呢?(我们先忽略TA1到TB0的指向箭头)
自然地,我们可以想到:Key的取值应当和最新的更改保持一致。但是由于很难实现一个所有节点都一致的全局时钟,所以不同节点各自的时钟实际上并不具有可比性,即TA1<TB1也不能说明B对Key的更改是在A之后发生的。从逻辑时钟的角度来看,TA1和TB1两者之间是没有因果关系的,无法判断哪个先发生。
我们可以发现Lamport逻辑时钟算法中每个进程只拥有自己的本地时间,没有其他进程的时间,导致无法描述事件的因果关系。如果每个进程都能够知道其他所有进程的事件,很大可能可以得到事件的因果关系。为此,有人提出了向量时钟算法,在Lamport逻辑时钟的基础上进行了改良,提出了一种在分布式系统中描述事件因果关系的算法。
基于向量时钟,我们可以获得任意两个事件的顺序关系,结果要么是有因果关系(先后顺序),要么是没有因果关系(同时发生)。通过向量事件,我们能够识别到如果两个数据更新操作是同时发生的关系,那么说明出现了数据冲突。
什么是向量时钟
通过上面的分析我们知道向量时钟算法是在Lamport逻辑时钟的基础上进行了改良,用于分布式系统中描述事件因果关系的算法。之所以叫做向量时钟,是因为向量时钟算法利用了向量(vector)这种数据结构将全局各个进程的逻辑时间戳广播给各个进程,我们需要每个进程都能够知道其他所有进程的时间,这样就能够通过计算得到事件的因果关系。每个进程发送事件时都会将当前进程已知的所有进程时间写入到一个向量中,并附带在发送的消息中。这就是向量时钟的由来。
再详细解释一下,向量时钟方法在分布式系统中用于保证操作的有序性和数据的一致性。向量时钟通常被认为是一组来自不同节点的时钟值Vi[1]、Vi[2]、…、Vi[n]。在分布式系统中,编号为i的节点维护某一数据的时钟时,根据这些值可以知道其他节点或副本的状态。例如Vi[0]是编号i节点所了解的编号0节点上的时钟值,而Vi[n]是编号i节点所了解的n节点上的时钟值。时钟值代表了节点上数据的版本信息,该值可以是来自节点本地时间的时间戳或者是根据某一规则生成有序数字。
以3副本系统为例,该系统包含节点0、节点1和节点2.某一时刻的状态可由表1-1来表示。
表1-1
表格中表示当前时刻各节点的向量时钟为:
节点0:V0(4,2,0)
节点1:V1(1,4,0)
节点2:V2(0,0,1)
在表1-1中,Vi代表编号i节点上的时钟信息,Vi[j]表示i节点所了解的j节点的时钟信息。以第2行为例,该行为V1节点的向量时钟(1,4,0),“1”表示V1节点所了解的V0节点上的时钟值;“0”表示V1节点所了解的V2节点上的时钟值;“4”表示V1节点自身所维护的时钟值。
向量时钟具体算法实现
-
初始时,我们将每个节点的值设置为0.每当有数据更新发生,该节点所维护的时钟值将增长一定的步数d,d的值通常由系统提前设置好。该规则表明,如果操作a在操作b之前完成,那么a的向量时钟大于b向量时钟值。
向量时钟根据以下两个规则进行更新。
-
规则2:在节点i的数据更新之前,我们对节点i所维护的向量Vi进行更新:Vi[i]= Vi[i]+d(d > 0)。该规则表明,当Vi[i]处理事件时,其所维护的向量时钟对应的自身数据版本的时钟值将进行更新。
-
规则3:当节点i向节点j发送更新消息时,将携带自身所了解的其他节点的向量时钟信息。节点j将根据收到的向量与自身所了解的向量时钟信息进行对比,然后进行更新:Vj[k] = max{Vi[k], Vj[k]}。在合并时,节点j的向量时钟每个维护的值取节点i与节点j向量时钟该维度值的较大者。
两个向量时钟是否存在偏序关系,通过以下规则进行比较:对于n维向量来说,Vi>Vj,如果任意k(0≤k≤n)均有Vi[k] >= Vj[k],就是Vi中的每一维都大于等于Vj向量,那么就说Vi,Vj向量之间存在偏序关系。而如果Vi既不大于Vj且Vj也不大于Vi,这说明两个向量之间不存在偏序关系,在并行操作中发生了冲突,这时需要采用冲突解决方法进行处理,比如合并。
向量时钟主要用来解决不同副本更新操作所产生的数据一致性问题,副本并不保留客户的向量时钟,但客户有时需要保存所交互数据的向量时钟。如在单独读一致性模型中,用户需要保存上次读取到的数据的向量时钟,下次读取到的数据所维护的向量时钟则要求比上一个向量时钟大(即比较新的数据)。
向量时钟的主要优势在于:
节点之间不需要同步时钟,即不需要全局时钟。不需要再所有节点上存储、维护一段数据的版本数。
下面我们通过一个例子来体会向量时钟如何维护数据版本的一致性。
A、B、C、D四个人计划下周去爬长城。A首先提议周三去,此时B给D发邮件建议周四去,他俩通过邮件联系后决定周四去比较好。之后C与D通电话后决定周二去。然后A询问B、C、D三人是否同意周三去,C回复说已经商量好了周二去,而B则回复已经决定周四去,D又无法联系上,这时A得到不同的回复。如果他们决定以最新的决定为准,而A、B、C没有记录商量的时间,因此无法确定什么时候去怕长城。
下面我们使用向量时钟来保证数据的一致性:为每个决定附带一个向量时钟值,并通过时钟值得更新来维护数据的版本。在本例中我们设置步长d的值为1,初始值为0.
- 在初始状态下,将四个人(四个节点)根据规则1将自身所维护的向量时钟清零,向量时钟值分别为:A(0,0,0,0)、B(0,0,0,0)、C(0,0,0,0)、D(0,0,0,0)
- A提议周三出去:A首先根据规则2对自身所维护的时钟值进行更新,同时将该向量时钟发往其他节点B、C、D节点。B、C、D节点在接收到A所发来的时钟向量后发现它们所知晓的A节点向量时钟版本(0,0,0,0)已经过时,因此同样进行更新。更新后的向量时钟值分别为:A(1,0,0,0)、B(1,0,0,0)、C(1,0,0,0)、D(1,0,0,0)
- B和D通过邮件进行协商:B觉得周四去比较好,那么此时B首先根据规则2更新向量时钟版本B(1,1,0,0),然后将向量时钟信息发送给D(1,0,0,0)。D通过与B进行版本对比,发现B的数据较新,那么D根据规则3对向量时钟更新:A(1,0,0,0)、B(1,1,0,0)、C(1,0,0,0)、D(1,1,0,0)
- C和D进行电话协商:C觉得周二去比较好,那么此时C首先更新自身向量时钟版本C(1,0,1,0),然后打电话通知D(1,1,0,0)。D根据规则3对向量时钟更新:A(1,0,0,0)、B(1,1,0,0)、C(1,0,1,0)、D(1,1,1,0).最终通过对各个节点的向量时钟进行比对,发现D的向量时钟与其他节点相比具有偏序关系,其中每个维度的向量都大于等于其他节点每个维度的向量值。因此该系统决定周二一起去爬山。
图3
该方法中数据版本可能出现冲突,即不能确定向量时钟的偏序关系。如图3所示,假如C在决定周三爬山后并没有将该决定告诉其他人,那么系统在此刻将不能确定某一数据向量时钟的绝对偏序关系。比较简单的冲突解决方案是随机选择一个数据的版本返回给用户。而在Dynamo中系统将数据的不一致性冲突交给客户端来解决。当用户查询某一数据的最新版本时,若发生数据冲突,系统将把所有版本的数据返回给客户端,交由客户端进行处理。
向量时钟的主要缺点
主要缺点就是向量时钟值的大小与参与的用户有关,在分布式系统中参与的用户很多,随着时间的推移,向量时钟值会增长到很大。一些系统中为向量时钟记录时间戳,某一时间根据记录的时间对向量时钟进行裁剪,删除最早记录的字段。
向量时钟在实现中有两个主要问题:
- 如何确定持有向量时钟的用户
- 如何防止向量时钟随着时间不断增长