本文是《如何学习分布式系统》中,关于时钟的相关介绍。
物理时钟的无奈
大家可能都知道,计算机的时钟过一段时间就不准了,为什么计算机的时钟不能保持统一的速度前进呢?因为计算机的时钟速度取决于它的石英振荡器,计算机上的石英振荡器一般来说精度都不是很高(因为精度高了就贵了。。。),在各种物理条件发生变化时有可能频率也发生变化(计算机全速运行一个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() and TT.before(),有个有趣的故事。读论文的时候,里面只说了一句——“The TT.after() and TT.before() methods are convenience wrappers around TT.now()”。我一开始想它们莫非就是简简单单的和TT.now()两个端点比较一下?以after方法为例,TT.after(t)应该就是TT.now().earliest > t,而TT.after(t)为true的意思是“t has definitely passed”,就是“t肯定已经过去了”,如果都已经“definitely”了,那么以下两种情况如何理解:
- 同一时刻,t在某些服务器上肯定已经过去了,而另一些服务器上不能肯定。
- t在某台服务器肯定已经过去了,过了一会儿,可能又不能肯定了。真是出尔反尔啊。
所以我想是不是Google有一些更黑科技的实现方法呢?于是分别去知乎和Stackoverflow提问。直到最后我看到了下面这个图,原来真的就是和TT.now()两个端点比较啊,看来是有点过度解读了。。。
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会有更复杂的步骤,这里就不深入展开了。
更多相关内容,请参考系列文章《如何学习分布式系统》。
共同学习,写下你的评论
评论加载中...
作者其他优质文章