建议先看论文原文再来看这篇文章,我不会对论文中的各个点都具体说明。仅仅是写一些我自己的想法,帮助理解。
大家都知道。分布式环境下。确定各个事件发生的顺序非常重要,不然就会发生一些麻烦的问题。
考虑一下这个问题:小明要用同学给的优惠码在京东上买一本书。如果京东的后台架构如图一所看到的。A是前端代理server,负责接收用户请求,B是优惠码验证server,当用户请求使用优惠码的时候,A会把请求发给B。让B来验证。C是日志server。专门存放日志。系统中的不论什么一个操作都会记录到日志server中。非常显然,这是一个分布式系统。
图一 后台架构
小明的买书过程分为3个步骤:
- 请求到达A。A发送一条消息给C,让C记录一条日志[小明下了订单XXX号]
- A是异步工作的。发送完1后,A发现小明使用了优惠码,就把优惠码发送给B进行验证
- B验证了优惠码发现是合法的。就发送消息给C,使之记录日志[XXX号订单使用了优惠码YYY]
至此,买书过程结束。假设一切正常的话,那日志server上会依次产生[小明下了订单XXX号],[XXX号订单使用了优惠码YYY]这两条日志。
但事情没有这么简单,假设A跟C之间的网络比較慢,导致3在1之前到达C,那么C上就会产生顺序相反的两条日志。这样就出问题了。由于是1的发生才导致了3的发生,这两者能够看成是有因果关系的,可是生成的日志却是相反的。
有什么办法能让系统识别事件的因果关系呢?比較easy想到的是让server每次发送消息的时候带上时间戳,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是两个独立的进程,也能够理解为分布式系统中的两台server;时间从上往下递增。圆圈表示事件。
图中红色的箭头就是上述的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)就没有意义了?换个角度想想,既然上面说过,并发事件的顺序不重要。不会影响系统的正确性。那么我们任意定一个顺序不就完了吗?就当作时间戳大的事件肯定是发生在后面,时间戳小的事件肯定是发生在前面,这样一来不就统一了因果事件和并发事件的排序了吗?可是另一个问题,两个并发事件的时间戳一样怎么办?那就再加一层假定,给分布式系统中的server编号。当两个时间戳一样时,编号小的server就当他发生在先.
总结一下上面说的,我们能够定义这样一个全序关系”=>”:如果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.
可是至少这种方法给出了一个定义分布式系统中事件顺序的方法,他确保的是全部因果关系的事件不会发生逻辑错误,但他并不保证系统的公平性(比方两台server同一时候并发地请求一个资源,物理时间上先发出请求的进程不一定会先得到这个资源。但这顶多会造成不公平,不会造成错误)。
怎么理解呢?看一下以下两个样例。
Logical Clock应用
如今让我们回过头看看文章开头提到的问题。怎么利用Logical Clock保证日志的顺序正确呢?Lamport在论文的后半部分提出了一个算法,能够解决问题。
可是这个算法基于一个前提:对于随意的两个进程Pi和Pj,它们之间传递的消息是依照发送顺序被接收到的。这个如果并只是分,TCP就能够满足要求。
- 首先。每一个进程会维护各自在本地维护一个请求队列。
算法是由例如以下5个规则定义的。
方便起见,每条规则定义的行为会被做为一个独立事件。
- 为请求该项资源(在这个问题中,资源就是日志server),进程Pi发送一个(Tm,Pi)资源请求消息给其它全部进程,并将该消息放入自己的请求队列,在这里Tm代表了消息的时间戳
- 当进程Pj收到(Tm,Pi)资源请求消息后,将它放到自己的请求队列中。并发送一个带时间戳的确认消息给Pi。(注:假设Pj已经发送了一个时间戳大于Tm的消息,那就能够不发送)
- 释放该项资源时。进程Pi从自己的消息队列中删除(Tm,Pi)资源请求。同一时候给其它全部进程发送一个带有时间戳的Pi资源释放消息
- 当进程Pj收到Pi资源释放消息后,它就从自己的消息队列中删除(Tm,Pi)资源请求
- 当同一时候满足例如以下两个条件时,就将资源分配给进程Pi:
a) 依照“=>”关系排序后。(Tm,Pi)资源请求排在它的请求队列的最前面
b) Pi已经从全部其它进程都收到了时间戳>Tm的消息
为什么这个算法能够保证日志server能被依照正确的顺序分配呢?细致想一想5中的a b两个条件,仅仅有当一个进程收到全部其它进程>Tm的消息后才会对自己的队列进行排序。
那么如果其它进程在他之前请求了这个资源,可是由于网络慢还没收到,怎么办?由于前面已经提出如果,对于随意的两个进程Pi和Pj,它们之间传递的消息是依照发送顺序被接收到的。
所以既然收到>Tm消息,那么说明其它全部进程在Tm之前的消息也都已经被收到了,所以这个时候自己的队列中肯定已经收到了全部的对资源的请求。这个时候仅仅须要依照“=>”关系排序。排在最前面的就是最先发出请求的。
可见利用Logical Clock确实能够解决这样的问题。那么我们再来看还有一种问题。如果京东的后台架构例如以下图所看到的:
图四 还有一种情况
有多台前端代理server。用户请求会随机地分配到各个代理server上。
假设小明在物理时间7点50下单买了一本书,大明在7点51分下单,正好书仅仅有一本。小明的请求被分配到了serverA,大明的被分配到了serverB。假设在这之前,A的Logical Clock走到了200。而B的Logical Clock走到150。那么在这个情况下。假设运用上述算法,B会先获得资源,下单买到票。所以从道理上说。这个算法是不公平的,可是换个角度想想,这样充其量也仅仅是不公平。不会导致系统发生因果错误。由于小明和大明的请求在系统看来是并发事件,没有因果关系,所以系统无法判定并发事件的真实顺序。
这就是之前我说的“这种方法给出了一个定义分布式系统中事件顺序的方法,他确保的是全部因果关系的事件不会发生逻辑错误,但他并不保证系统的公平性(比方两台server同一时候并发地请求一个资源。物理时间上先发出请求的进程不一定会先得到这个资源。但这顶多会造成不公平,不会造成错误)。
”
參考文献:
- Lamport, L. (1978). “Time, clocks, and the ordering of events in a distributed system”
- Why vector clocks are more powerful than Lamport timestamps
- Lamport论文中文翻译加感想
- 数据一致性: 时间戳策略
- 分布式数据库的数据版本号合并