分布式系统之不可靠时钟揭秘

时钟是一个我们常常会使用的东西,比如我们会用它来确定一个事情发生的时间或者说一个请求花费的时间。然而在分布式系统中,每个机器都有他们自己的时钟,通常来说是由它们本身的硬件来决定的(比如晶振等),它们都不是精确准确的,所以每个机器之间的时钟都或多或少有点差别。所以当我们需要使用不同机器的时钟时,比如比较两个发生在不同机器上事情发生的先后顺序的时候,就很难说哪一个事情是真正先发生哪一个是后发生的。本文就来介绍一下一般如何处理这个问题。

单调时钟(Monotonic)和当天的时间(Time-of-Day Clock)

在现代计算机上,一般有两种时钟,一个是单调时钟另外一个是当天时间。虽然两者都是时钟,但它们其实有很大的差别。

当天时间

所谓当天时间就是我们通常意思上看到的时间,比如现在是几月几日几点几分等。从代码的角度来讲就是类似clock_gettime(CLOCK_REALTIME)返回的值,通常来说这个值会和NTP(Network Time Protocol)进行同步,所以理想状况下各个机器这个值都是差不多,当然现实中每个机器还是不同的,或多或少会有漂移。不过假如漂移过大就会被强制重置从而跳回正确的时间。然而正是因为这种漂移和重置的机制使得这个时钟不太适合用来计算消耗的时间,因为他会不准设置有可能会变成负值。

单调时钟

单调时钟顾名思义就是这个时钟始终往前单调递增。这样一来它就很适合来衡量过去了多长时间。比如说你想看一个请求的时间,可以在发送请求前得到一个时间,然后在得到response之后再获取一个时间,连个时间的差就是你想要的duration。

需要注意的是这个时间的绝对值没有任何意义,所以你要是去比较多个机器的单调时钟是不合适的。

另外对于多CPU的服务器,其实没个CPU的单调时钟也是不同的,只是说一般来说操作系统会帮应用程序处理这个不同,这样一来应用程序就不用担心这个问题。但是有时这个保证也不一定对,所以知道这件事有可能会对一些意想不到的问题有帮助。

NTP对单调时钟也是有影响的,它会看本地的单调时钟的频率,假如本地时钟太快或者太慢它会进行调整(一般的容忍度在0.05%),但是和当天时间不同的是,它只会调整频率,不会让单调时钟跳到很前或者很后。所以,一般来说单调时钟还是一个很好的方法来测量消耗的时长,毫秒级别的精度是没有问题。

时钟的同步和准确性

当天时间是需要进行同步的,因为只有这样它才有真正的意义。然而时钟的同步可能和我们想象的不太一样,我们来看几个例子:

  • 机器上的晶振本身是不准确的,它会漂移。这是一个硬件上的问题,主要会跟环境的温度等等息息相关。Google一般会假设他们的服务器有200ppm的漂移,相当于每30秒有6ms的差别。所以哪怕别的方面都没有问题,这个漂移都是存在的。
  • 机器的时钟和NTP有很大差别的时候,可能会拒绝同步或者被强制重置。这样一来我们就会看到时钟会有一个向前或者向后的跳跃。
  • 假如机器的防火墙设置有问题,那么时钟可能就没有机会和NTP来进行同步了,而我们都不知道这件事。
  • 即使你能够和NTP同步,这个准确度也会因为你的网络延时而有差别。可以想象假如同步的信息收到了网络传输的影响,那么时钟的同步就是不准确的。
  • NTP服务器本身可能也会有问题,这样一来你拿到的数据可能也不是准确的。
  • 跳跃的秒数会让一分钟的时间发生变化,再也不是60s了,也就是说一分钟可能是59s也可能是61秒,这就很有可能导致系统出现问题。
  • 虚拟机的时间更加复杂,因为多个虚拟机共享CPU,每一个虚拟机在别的机器运行的时候可能都需要暂停几个毫秒,这就使得时间的计算更加困难。

所以精确的时钟在现实中是很难得到的,但是我们仍然可以做一些努力来得到相对精确的时钟,比如依赖于GPS的PTP (Precision Time Protocol),然后很好的控制deploy和监控。当然这样做的代价也很大。

依赖同步时钟的实例

虽然我们知道时钟有这样那样的问题,但是我们仍然有很多情况想要依赖于这个时钟,这该怎么办呢?下面我们从几个例子来具体分析如何处理

有序事件的时间戳

一个常见的例子就是多个节点同时写一个值,常见的做法就是比较一下谁先写谁后写,后写的失败或者覆盖先写的。如下图所示,一个多leader的数据库的写:

Client A在Node 1上写了x=1, 然后这个值被replica到节点3。之后Client B操作节点3,增加了1,所以这个时候x变成了2。最后这两个操作都replica到了节点2。这里我们看到Node1和Node3之间的时间差是小于3ms,这个是一个很正常的差别。而这导致的结果就是在节点2上,当他进行判断哪个是后写的时候出现了错误,因为从节点1过来的值理论上应该是先发生的,但是它上面带着的时间是42.005,而节点3过来的值虽然是后发生的,但是它的时间戳是42.004,这就出现了一个相反的顺序。从而导致节点2上的判断出现了错误。

这就是LWW(Last write win)这一机制的问题,这一机制简单来说就是后写的覆盖先写的,这里的问题就是如何判断谁是后写的,因为节点之间的时间戳事实上可能是不准的,这就是导致简单的时间比较是不够精确的。所以很多时候我们不会选择时间戳来进行比较,而是使用一个称之为逻辑时钟的东西,它是基于一个一直在增加的数来进行比较,可以用来比较两个event的相对时间,这其实就足够我们使用了。

时钟的置信区间

我们使用函数读取时间戳的时候,经常可以看到精度,比如我们可以读取到毫秒级的精度,甚至纳秒级的精度。那么这是否意味着我们读到的时间就真的精确到这个级别呢?其实不然,正如我们上面说到的各种时间的漂移,事实的时间是有一个置信区间的,比如说95%的概率现在的时间是在10.3到10.5秒之间,但是不知道具体是哪个。所以假如置信区间是100ms级别的,显然你读到的纳秒值就没有什么意义了。

这个置信区间可能是由你系统的已经决定的,比如你看晶振的spec,它会有一个左右的标准值之类的。然而,很不幸的是,这些东西都不会真的暴露给开发者,比如你调用一个系统函数clock_gettime,它就会返回一个时间戳,它不会告诉你时间的置信区间。

当然也有API会告诉你这个东西,比如Google的Truetime API会返回一个最早值和最晚值,其实这就是置信区间。

使用同步时钟来进行全局的snapshot

我们之前讨论过snapshot隔离,这里有一个全局Transaction ID用来判断各个transaction会否在snapshot之前还是之后发生。有一种实现方案就是使用同步的时钟,它使用了我们上面提到的Google true Time API,这样就得到了两个区间的时钟,假如他们没有交错,那么就可以简单判断他们的先后了,假如有交错,就认为无法确定。所以为了让这个API返回的值够精确,Google甚至在每一个数据中心部署了一个GPS的接收器,这样可以更精确地进行时钟的同步。

总结

本文介绍了我们最常见的时钟,揭秘了它背后我们平时完全没有在意的问题。希望大家阅读之后能够对分布式系统中时钟有更加深刻的理解。

You may also like...

1 Response

  1. July 10, 2021

    […] 讨论中提到的client和server端时间不同的相关知识:《分布式系统之不可靠时钟揭秘》 […]

Leave a Reply

Your email address will not be published. Required fields are marked *