我对Lamport Logical Clock的理解
作者: on 星期二, 九月 3, 2013 · 【阅读:4,998 次】
环境中的一致新问题一直是最热门的话题之一,本文主要介绍了其中的一种比较简单的思路:。本文来自@ 博客文章的投稿。感谢他的分享。
原文地址:
建议先看论文原文再来看这篇文章(原文见文章下方参考文献部分),我不会对论文中的各个点都详细说明,只是写一些我自己的想法,帮助理解。
大家都知道,分布式环境下,确定各个事件发生的顺序很重要,不然就会发生一些麻烦的问题。
考虑一下这个问题:小明要用同学给的优惠码在京东上买一本书。假设京东的后台架构如图一所示,A是前端代理服务器,负责接收用户请求,B是优惠码验证服务器,当用户请求使用优惠码的时候,A会把请求发给B,让B来验证。C是日志服务器,专门存放日志,系统中的任何一个操作都会记录到日志服务器中。很显然,这是一个分布式系统。
图一 后台架构
小明的买书过程分为3个步骤:
请求到达A,A发送一条消息给C,让C记录一条日志[小明下了订单XXX号]
A是异步工作的,发送完1后,A发现小明使用了优惠码,就把优惠码发送给B进行验证
B验证了优惠码发现是合法的,就发送消息给C,使之记录日志[XXX号订单使用了优惠码YYY]
至此,买书过程结束。如果一切正常的话,那日志服务器上会依次产生[小明下了订单XXX号],[XXX号订单使用了优惠码YYY]这两条日志。但事情没有这么简单,如果A跟C之间的网络比较慢,导致3在1之前到达C,那么C上就会产生顺序相反的两条日志。这样就出问题了,因为是1的发生才导致了3的发生,这两者可以看成是有因果关系的,但是生成的日志却是相反的。
有什么办法能让系统识别事件的因果关系呢?比较容易想到的是让服务器每次发送消息的时候带上时间戳,C收到消息之后比对时间戳,就知道谁先谁后了。问题是,在分布式环境下,各个机器的时钟是无法100%同步的,所以这种方法不靠谱。
那有没有办法在不借助物理时钟的情况下给分布式环境下的所有事件排序呢?
这就需要借助Lamport大神在1978年提出的Logical Clock。
Happend-before关系
Lamport在文章中首先定义了一种关系,叫做Happend-before,我个人的理解是相当于因果关系,用->表示,比如a->b就叫做a Happend-before b,即b是由a引起的。 若两个事件a,b满足下面任一条件,则记作a->b:
如果a b是在同一进程内的两个事件,并且a发生在b之前,那么a->b
如果a代表了某个进程的消息发送事件,b代表另一进程中针对这同一个消息的接收事件,那么a->b
下面这张图很好地展示了Happend-before这一关系。
图二 Happend-before关系
P和Q是两个独立的进程,也可以理解为分布式系统中的两台服务器;时间从上往下递增,圆圈表示事件。图中红色的箭头就是上述的Happend-before关系。a,b和e,f分别是同一进程中的先后两个事件,满足条件1,b和f分别是消息的发送事件和接受事件,满足条件2。那么事件a和e,b和e,分别是什么关系呢?其实他们之间没有因果关系,可以认为他们是并发的。正因为他们没有因果关系,所以从系统的角度来看,他们谁发生在前谁发生在后不重要,只要能辨别清楚有因果关系的事件,使得因果关系不会倒转,那么整个分布式系统就可认为是正确的,至少从逻辑上不会很出错。至于那些没有因果关系的并发事件,不用关注他们的先后顺序。(理解这段话是很重要的,这正是Logical Clock的精髓所在。)
可以这么理解,Logical Clock解决的问题是找到一种方法,给分布式系统中所有时间定一个序,这个序能够正确地排列出具有因果关系的事件(注意,是不能保证并发事件的真实顺序的),使得分布式系统在逻辑上不会发生因果倒置的错误。
那么如何利用Happend-before关系来定义顺序呢?继续往下看。
Logical Clock
现在我们给系统中所有的事件打上一个时间戳(其实就是一个递增的序号)。每个进程维护一个自己的时间戳,时间戳的增加遵循下面两点规则:
如果两个事件发生在同一个进程上,并且不是接受消息的事件,那么后面事件的时间戳为前面的+1
如果一个事件是接受消息,那么他的时间戳为本进程前续事件的时间戳与接受到的消息的时间戳中较大者+1
图三 时间戳演示
上图展示了打完时间戳后的样子,可见这个时间戳确保了在因果条件的事件中是递增的。但是并发的事件(如a和e),他们的时间戳是没有可比性的,谁大谁小说明不了问题。记C(a)为事件a的时间戳,那么:
若a->b,即a与b有因果关系,那么C(a)>C(b)
若a与b没有因果关系,那么C(a)与C(b)可能是任意关系(大于 小于 等于)
也就是说,根据这个时间戳,是没法反过来推断事件发生的真实顺序的,因为对于并发事件来说,虽然C(a)>C(b),但也许a与b的真实顺序是t(a) < t(b)。
那么C(a)>C(b)就没有意义了?换个角度想想,既然上面说过,并发事件的顺序不重要,不会影响系统的正确性,那么我们随意定一个顺序不就完了吗?就当作时间戳大的事件肯定是发生在后面,时间戳小的事件肯定是发生在前面,这样一来不就统一了因果事件和并发事件的排序了吗?但是还有一个问题,两个并发事件的时间戳一样怎么办?那就再加一层假定,给分布式系统中的服务器编号,当两个时间戳一样时,编号小的服务器就当他发生在先。
总结一下上面说的,我们可以定义这样一个全序关系”=>”:假设a是进程Pi中事件,b是进程Pj中的事件,那么当且仅当满足如下条件之一时:(1)Ci(a)<Cj(b);(2)Ci(a)=Cj(b)且Pi<Pj,那么我们就认为“a=>b”。
这样一来,我们就完成了对分布式系统中所有事件的定义。但是大家可能会疑惑,这样是不是有点太随意了,很不靠谱的感觉。确实,Lamport也承认这一点,他在论文最后也说了:
The total ordering defined by the algorithm is somewhat arbitrary. It can produce anomalous behavior if it disagrees with the ordering perceived by the system’s
users.
但是至少这个方法给出了一个定义分布式系统中事件顺序的方法,他确保的是所有因果关系的事件不会发生逻辑错误,但他并不保证系统的公平性(比如两台服务器同时并发地请求一个资源,物理时间上先发出请求的进程不一定会先得到这个资源。但这顶多会造成不公平,不会造成错误)。怎么理解呢?看一下下面两个例子。
Logical Clock应用
现在让我们回过头看看文章开头提到的问题,怎么利用Logical Clock保证日志的顺序正确呢?Lamport在论文的后半部分提出了一个,可以解决这个问题。但是这个算法基于一个前提:对于任意的两个进程Pi和Pj,它们之间传递的消息是按照发送顺序被接收到的。这个假设并不过分,TCP就可以满足要求。
首先,每个进程会维护各自在本地维护一个请求队列。算法是由如下5个规则定义的。方便起见,每条规则定义的行为会被做为一个独立事件。
为请求该项资源(在这个问题中,资源就是日志服务器),进程Pi发送一个(Tm,Pi)资源请求消息给其他所有进程,并将该消息放入自己的请求队列,在这里Tm代表了消息的时间戳
当进程Pj收到(Tm,Pi)资源请求消息后,将它放到自己的请求队列中,并发送一个带时间戳的确认消息给Pi。(注:如果Pj已经发送了一个时间戳大于Tm的消息,那就可以不发送)
释放该项资源时,进程Pi从自己的消息队列中删除(Tm,Pi)资源请求,同时给其他所有进程发送一个带有时间戳的Pi资源释放消息
当进程Pj收到Pi资源释放消息后,它就从自己的消息队列中删除(Tm,Pi)资源请求
当同时满足如下两个条件时,就将资源分配给进程Pi:
按照“=>”关系排序后,(Tm,Pi)资源请求排在它的请求队列的最前面
Pi已经从所有其他进程都收到了时间戳>Tm的消息
为什么这个算法可以保证日志服务器能被按照正确的顺序分配呢?仔细想一想5中的a b两个条件,只有当一个进程收到所有其他进程>Tm的消息后才会对自己的队列进行排序。那么如果其他进程在他之前请求了这个资源,但是由于网络慢还没收到,怎么办?因为前面已经提出假设,对于任意的两个进程Pi和Pj,它们之间传递的消息是按照发送顺序被接收到的。所以既然收到>Tm消息,那么说明其他所有进程在Tm之前的消息也都已经被收到了,所以这个时候自己的队列中肯定已经收到了所有的对资源的请求,这个时候只需要按照“=>”关系排序,排在最前面的就是最先发出请求的。
可见利用Logical Clock确实可以解决这种问题。那么我们再来看另一种问题。假设京东的后台架构如下图所示:
图四 另一种情况
有多台前端代理服务器,用户请求会随机地分配到各个代理服务器上。如果小明在物理时间7点50下单买了一本书,大明在7点51分下单,正好书只有一本。小明的请求被分配到了服务器A,大明的被分配到了服务器B。如果在这之前,A的Logical Clock走到了200,而B的Logical Clock走到150。那么在这个情况下,如果运用上述算法,B会先获得资源,下单买到票。所以从道理上说,这个算法是不公平的,但是换个角度想想,这样充其量也只是不公平,不会导致系统发生因果错误。因为小明和大明的请求在系统看来是并发事件,没有因果关系,所以系统无法判定并发事件的真实顺序。
这就是之前我说的“这个方法给出了一个定义分布式系统中事件顺序的方法,他确保的是所有因果关系的事件不会发生逻辑错误,但他并不保证系统的公平性(比如两台服务器同时并发地请求一个资源,物理时间上先发出请求的进程不一定会先得到这个资源。但这顶多会造成不公平,不会造成错误)。”
参考文献:
Lamport, L. (1978).
system”