Design Data-Intensive Applications 读书笔记二十四 第八章:不可靠的时钟

不可靠的时钟

系统中会用到时间段和时间点。两种用法:获取事件点和时间段。在分布式系统中,通信需要时间,因为延迟,难以知道分布式系统事件的发生顺序。而且机器都使用石英钟,不同节点多少存在不一致,这个不一致一般使用网络授时来解决。

对应时钟的两种用法,有两种类型的计时方法,日期和计数。

 

时钟同步和准确性

计数时钟不需要授时,但是日期时钟需要,但是因为硬件的关系,时钟往往不准确。

Google认为时钟偏移200ppm(一百万分之一),每30s就有6ms的偏移,一天有17s需要校准。使用授时系统也会出现网络问题,而且部分授时系统也不一定准确。

 

时间戳与事件顺序

分布式系统会依据时间戳来对事件排序如图8-3所示的使用时间戳来确定写入顺序:

Design Data-Intensive Applications 读书笔记二十四 第八章:不可靠的时钟

如图所示,如果不同节点的时间都是同步,但是如果如时钟有差异,那么事件的顺序与实际不符。如图:按照时间戳排序,clientB将x加一后,然后clientA会将x置为1,client B的操作会丢失;但是实际情况是clientB的增长操作在clientA的set操作之后。这种操作遵循LWW(最后写入胜出原则),多用于解决多节点的并发冲突。如果时钟不一致,使用时间来做判断的逻辑会出问题,所以LWW相关的操作也会出错。这会造成如下错误:

1、很多写入会莫名丢失。2、写入顺序无法判断,并发控制会出问题。3、处理并发值,两个节点可能在相同时间生成相同时间戳,需要决定它们写入的顺序,这可能造成逻辑错误。

网络时间协议(NTP)无法解决时钟问题,因为NTP的精度受限于网络传输时间,也与石英钟的精度有关。如果需要确定顺序,使用的时钟需要比要测量的对象(如:网络延迟)的精度高。

为此,使用逻辑时钟(即只增的计数器,而不是石英钟),来处理顺序问题。逻辑时钟不测量时间,只是确定事件的顺序。

 

时间置信区间

NTP的误差一般是几十毫秒,但是因为网络拥挤问题,会超过100毫秒。所以获取时间点没有多大意义,获取一个时间范围更有用。例如:一个系统处在这分钟的10.3s和10.5s之间的概率是95%,同时无法知道准确值。如果我们知道时间波动的范围是100ms,那么毫秒精度的时间戳没有意义。

时间精度受限于数据源,如果使用GPS时,那么就是原子钟级别的精度,由生产厂商提供精度数据;如果是NTP,需要考虑NTP的精度和网络延迟。但是大部分情况下,我们没有办法获得时间的准确度,我们通常只能获得一个数据来表示时间点。一个例外就是Google的 Spanner系统的TrueTime提供的api,会返回两个值 [earliest, latest],表示可能的最早和最晚的时间。

 

同步时钟与全局快照

在之前的“事务”章节中,我们讨论了快照隔离,它允许事务读取一个时间点上的数据库的一个一致性状态,不需要加锁和妨碍读写事务。

实现快照隔离的最常用的方法需要只增的事务id。如果写入晚于快照,那么写入对于快照就是不可见的。对于单节点数据库,一个简单的计数器就足够了。

但是对于跨越多机器的分布式数据库,生成只增的事务id有些难,需要协调。事务id需要反映逻辑,如果事务B的id高于事务A,则B能读到A的写入,否则事务不一致。如果有很多短事务,创建事务id是分布式系统的一个槛。

是否可以用同步时钟的时间戳来作为事务id?如果能很好地同步,那么就能用,但问题是时钟的准确性。

Spanner实现事务id的方式如下:它使用了True Time API。如果你获得两个置信区间 A = [Aearliest, Alatest] 和 B = [Bearliest, Blatest] 这两个区间没有交集例如 Aearliest < Alatest < Bearliest < Blatest ,那么事件A先于事件B;如果有交集,那么无法判断顺序。为了能够反映逻辑,Spanner会有意等待置信区间那么长的时间后才提交事务。就是为了确保读取事务有足够的延迟,因为置信区间不会重叠。为了减少等待时间,需要缩小时间的不确定性,所以Google在每个数据中心部署了GPS接收器或者原子钟,将同步时钟的偏差缩小到了7ms以内。

使用时钟来作为分布式事务的语义还在研究中。在Google之外没有成为主流。

 

进程停顿

接下来是分布式系统中使用时钟的另一个危险案例:假设你使用了单主节点的数据库。只有主节点接受写入,那么其他节点如何知道主节点仍然是主节点?它是否依然能接受写入?

一个方法就是主节点每次从其他节点那里获取“租期”,类似于有时限的锁。一个时间只能有一个节点持有租期,因此获取锁时,有一个节点被承认为主节点,直到租期过期。为了维持主节点地位,需要时不时的在过期之前刷新租期。如果节点故障,那么租期就会过期,其他节点就会成为主节点。

可以认为请求循环类似于:

Design Data-Intensive Applications 读书笔记二十四 第八章:不可靠的时钟

上述做法有两个问题:1、使用同步时钟,之前已经说过,同步时钟并不准确。2、即便改用单调时钟,另一个问题就是认为检查的时间和处理请求的时间点之间时间很短;一般情况下运行时间短,10s足够,但是总有例外情况。如果 lease.isValid()语句周围执行了15s(线程暂停了15s),那么租期就会过期,其他节点就会被选为主节点。但是这个节点并不会收到通知,直到下一个循环,那时候就已经做出了部分修改了。一个线程暂停15s是有可能的:

1、垃圾收集,2、线程切换,3、IO操作等。这些情况都可能导致线程在任务时候暂停。分布式系统必须认为执行可能在任何地方暂停相当长的时间,即便是在方法内,而且长时间不会有响应;但是最后节点仍然能继续运行,但是直到后续时钟检测时,它不会发出通知它正在休眠。

 

 

限制响应时间

很多编程语言和操作系统,线程和进程可能会无限地暂停。如之前讨论的,这些是可以避免的,但是需要做很多工作。如果软件运行在一个特定时间内没有响应的环境下,会造成严重后果:如火箭,汽车,机器人等,或者其他需要快速反应的机器。在这些系统里,程序必须在一个期限内作出回应,否则会赞成严重后果,这些系统成为硬实时系统。系统提供实时保障需要软件栈全方位的保障,包括CPU,依赖库和内存分配等,已经大量的测试工作。所有的这些都需要大量的投入。所有实时系统很昂贵。并且“实时”并不意味着“高效”,实际上它们将相应时间看得最重要,所以他们的吞吐量比较低。对于服务端系统,实时系统不实用,也可能不合适。因此,服务端系统要面对暂停和不稳定的时钟。

 

限制垃圾收集

不用实时系统也可以减缓进程暂停的影响。一个新兴的想法就是在节点暂停的时候进行垃圾回收,然后让其他节点处理请求。如果运行环境警告节点将要进行垃圾回收,那么应用停止向节点发送请求。    这个想法的一个变种就是只使用垃圾回收器回收生存时间短的类,然后定期重启进程,这样能加速回收生存时间长的对象。一个节点可以定期重启,期间,流量会转发至其他节点。