物理时钟的无奈
大家可能都知道,计算 机的时钟过一段时间就不准了,为什么计算机的时钟不能保持统一的速度前进呢?因为计算机的时钟速度取决于它的石英振荡器,计算机上的石英振荡器一般来说精度都不是很高(因为精度高了就贵了。。。),在各种物理条件发生变化时有可能频率也发生变化(计算机全速运行一个for循环时的时钟速度和空闲时是不一样的),所以导致计算机时钟不能匀速前进。
现在一般都采用NTP之类的技术来保持时钟同步,不过由于网络延迟,同步频率等因素,物理时钟还是很难保持精确一致。用真实的时间作为横坐标来看,理想情况下,节点上时间应该是下图那样一条斜率为1的直线,真实情况则是它在这条直线上上蹿下跳。
对于分布式系统来说,时钟不能精确一致会有什么问题呢?假设在真实时间100的时候,一个操作X=200发生在节点A,节点记录时间为101,接着在真实时间101的时候一个操作Y=50发生在节点B,节点记录时间为98,那么从外部观测者看来,Y=50是发生在X=200前面了。
简单粗暴的“等”
如何处理上述情况呢,最简单的方法就是一个字——等!
要想使用“等”字诀,就要知道节点的时钟和真实时钟的误差的最大值。假设系统中节点时钟和真正的时钟最大误差为e,意味着在真正的t时刻,节点的时钟应该在[t-e, t+e]这个范围。完成一个操作之后,不管三七二十一,先等一会儿,再执行下一个操作,这样就保证了后一个操作的时钟大于前一个。要等多久呢?万无一失的操作是,每次等2e的时间就行了。
Google Spanner的“等”
Google Spanner就是用“等”来解决分布式系统的时钟问题的。按照论文中的说法,Spanner是Google的“scalable, multiversion, globally distributed, and synchronously replicated database”,它实现了external
consistency的一致性模型(按照论文中说法,就是linearizability),其方法就是利用TrueTime和“等”字诀(官方说法叫“Commit Wait”)。TrueTime有两方面的作用:
使e变得非常小
不用每次操作都等待e,而是最多等待e
TrueTime的实现
TrueTime的实现细节Google没有透露,论文里只是介绍说同时使用了GPS和原子时钟,因为GPS和原子时钟有不一样的失效模型。他们在数据中心内部署一组time master,大部分的master上有GPS接收器,剩下的master上则装备原子时钟(这些master叫做世界末日master。。。),master之间互相通信,按照特定的规则运行。在其他服务器上则有time slave,slave定期从master进行时间同步。所以服务器上的时钟误差一直在变化,刚同步完时误差最小,同步之前则误差最大。
如下图所示,TrueTime对外提供了三个API。
在一般的时间库中,时间都是以一个时间点的方式取得,而TT.now()返回的则是一个区间[earliest,latest]。TrueTime保证的是调用TT.now()的真实时间一定处于它返回的区间之内。
TT.after和TT.before在论文中被一笔带过——“The TT.after() and TT.before() methods are convenience wrappers around TT.now()”,按常理推断,TT.after(t)是TT.now().earliest t,而TT.before(t)是TT.now().latest t。
但是,TT.after()和TT.before()的说明里的“definitely”有点不太严谨。
按理说,同一时刻在不同的节点上调用TT.now(),返回的区间应该是不一样的,那么有的节点认为TT.after(t)是true,有的认为是false?也就是说,有些服务器上“t肯定已经过去了”,而有些服务器不是。
由于时钟误差一直在变化,所以TT.now()的区间的宽度也是在变化的。那么就有可能在一台服务器上,某个时刻TT.after(t)是true,在随后的某个时刻TT.after(t)变成false了?在一台服务器上,某一时刻,它认为“t肯定已经过去了”,但是过了一会儿,它又不肯定了,总是感觉有一种出尔反尔的感觉。
TrueTime的应用
Spanner中有以下四种操作,这里我们以简化版的“Read-Write Transaction”为例,说明一下TrueTime的使用。
在“Read-Write Transaction”操作中,Spanner想要完成的一致性模型是:如果事务T2的start发生在事务T1的commit之后,那么T2的commit时间必须比T1的commit时间大。这里,我们假设某个事件e的真实时间是tabs(e),事务i的start事件是eistart,事务i的commit事件是eicommit,事务i的commit请求到达服务器的事件为eiserver,事务i的commit时间是si,那么要达到的要求就是:如果tabs(e1commit) tabs(e2start),那么s1 s2。
Spanner的操作有两条规则:
Start:在事务i的eiserver事件发生之后,它产生一个时间si,要求si = TT.now().latest,所以si = tabs(eiserver) ,这个时间si将作为事务Ti的commit时间。
Commit Wait:产生si以后,它要一直等到TT.after(si)返回true之后再commit。这样si小于真实的commit时间,即si tabs(eicommit) 。
很容易证明如果tabs(e1commit) tabs(e2start),那么s1 s2:
由“Commit Wait”可知,s1 tabs(e1commit),所以s1 tabs(e2start)
显然tabs(e2start) tabs(e2server)
由“Start”可知,tabs(e2server) = s2
所以s1 s2
这样平均每次等待误差的平均值即可,而不是每次每次等待最大值。
当然如下图所示,实际上Spanner会有更复杂的步骤,这里就不深入展开了。