事件的先后顺序
一个业务系统能够正常运行,各个事件的时间先后顺序至关重要。后发生事件能够看到先发生事件的结果。如果A事件先发生,B事件后发生。但是由于时钟误差,导致B事件的时间戳先于A时间,这就会导致灾难性故障。
一个容易理解的例子就是银行取钱,小明银行卡100元,取钱10元然后查看余额。事件A就是取钱10元,事件B查看余额,结果为90元。但是事件B的时间戳先于A,那么小明在查询余额时就不会有A事件的结果,即查看时还是100元,但是实际已经转出了10元。这就是严重异常。
物理时钟
在单机应用中,我们不需要刻意去注意事件的先后顺序,或者说比较事件的先后顺序非常容易。当事件发生时,从本机读取时间,然后比较即可。
但是在分布式系统中,不同机器上的时间是有误差的。这是由于计算机一般通过晶振输出频率稳定的震荡信号来完成计时,但是工作温度,老化还有电压等因素都会影响晶振的工作频率,导致时间出现误差。
NTP协议
既然各个机器上时间存在误差,那么额外添加一个服务器,所有机器都从这个机器上来同步时间,理论上就可以保证所有机器时间准确了。NTP协议就是改解决方案。NTP客户端通过网络,不断轮询NTP server来同步时间。但是在实际应用中,由于网络时延对称性和其他因素的影响,常有毫秒级的时延,跨数据中心NTP同步可达上百毫秒。而且NTP协议也不对误差范围做任何保证,因此在时钟精度要求高的场景下,NTP时钟同步效果并不理想。
逻辑时钟
Lamport在上世纪70年代,就提出了使用Logic Clock来确定事件的先后顺序(偏序)。
简单的理解逻辑时钟是一串不断增长的number。如果事件a和事件b有以下关系:a -> b,那么逻辑时间C(a) < C(b)。至于如何确定a->b,需要额外看资料。
总之,逻辑时钟确实早一定程度上解决分布式系统的事件顺序问题,但是由于没有实际物理时间概念,无法通过真实时间来查询事件。
解决方案
分布式数据库要实现全局一致性快照,需要解决不同节点之间时钟一致的问题。工业界目前有3种解决方案:
- 全局集中式授时服务,对网络要求比较高,不能跨地域,理论上可以做到外部一致性;
- 混合逻辑时钟(HLC),可以保证同一个进程内部事件的时钟顺序,但是解决不了系统外事件发生的逻辑前后顺序与物理时间前后顺序的一致性,因此做不到Linearizability,也就做不到外部一致性;
- Google的物理时钟Turetime API,可以做到外部一致性,同时能做到全球化部署。
True Time
由于物理时钟和NTP协议都是存在误差,无法用来确定事件发生的时间。在Google spanner里面,通过引入True Time来解决了分布式时间问题。利用TrueTime, Spanner可以保证整个分布式系统中,在全球所有数据中心,所有事务分配到的timestamp都是具有可比性的, 基于同一个参考系的。
Commit Wait
Spanner在TureTime的基础上,还引入一个Commit Wait的限制, 保证了外部一致性(External Consistency), 也就是保证了如下规律:
如果事务T2在事务T1提交后, 才开始提交, 那么T2的commit timestamp肯定大于T1的commit timestamp。
API以及简单原理
Spanner通过使用GPS + Atomic Clock来对集群的机器进行校时,精度误差范围能控制在ms级别,通过提供一套TrueTime API给外面使用。
TrueTime API很简单,只有三个函数:
Method |
Return |
TT.now() |
TTinterval: [earliest, latest] |
TT.after(t) |
true if t has definitely passed |
TT.before(t) |
true if t has definitely not arrived |
首先now得到当前的一个时间区间,spanner不能得到精确的一个时间点,只能得到一段区间,但这个区间误差范围很小,也就是ms级别,我们用ε来表示,也就是[t - ε, t + ε]这个范围。
为事务分配时间戳
Spanner的读写事务会将时间戳作为版本号按照前面所说的格式写到记录中,那么现在问题在于:拥有TrueTime的情况下如何为事务Ti选择时间戳Si使得事务之间能满足外部一致?下面是Spanner做法:
- 在事务Ti决定提交的时候,选择TT.now().latest作为Si
- 提交时进行等待,直到TT.after(Si)为true,这被称为commit wait(2ε)。
TrueTime要解决的问题
Spanner是要解决全球规模分布式系统中关于时间的两大难题:
- 在数据中心之间同步时间是超级困难和不确定的
- 在全球规模范围内序列化请求是不可能的
TrueTime的结构
每个datacenter具有若干个time master, 另外每台设备上运行有timeslave进程. 前者是Spanner的时钟来源, 大部分是配备GPS接收器,剩下一小部分配备原子钟(防止GPS的天线故障或者频率干扰, 所以也叫"末日时钟").
全局集中式授时服务
TiDB采用全局集中式授时服务(Timestamp oracle,TSO)来解决分布式系统的时钟问题。
单点 TSO 服务
TSO 授时服务可以保证按照递增的方式分配时间戳,任何一次申请得到的时间戳都不会重复,在分布式系统中往往用于给事件定序,最常见和重要的作用即保证事务版本号的单调递增,确保分布式事务的时序。
在一个 TiDB 集群中,PD leader 节点会作为全局单点的授时服务进行时间戳分配。为集群的分布式事务,数据隔离级别,版本控制等提供稳固的基石,PD 的 TSO 服务性能也久经考验,可以轻松达到百万级别的 QPS。
TiDB的PD为了避免单点故障,使用了etcd的Raft构建集群。但是在网络分区或者其他原因导致触发选主时,在选举过程中的不可用无法避免。
然后就跨机房带来的网络延时问题。如果延时过高,对集群性能将有很大影响。
解决方案
将事务区分成本地事务和全局事务。
本地事务(Local Transaction )只被允许访问本地中心的数据。由于此时授时服务TSO和TiDB在一个数据中心,无跨中心延迟。性能非常好。
全局事务可以访问其他数据中心的数据,有全局的TSO进行授时,需要做同步计算。