最近在写关于 Date/Time 的代码,不由得想起一件发生在二零一七年的往事,重温一下 Wall Clock 和 Monotonic Clock。

什么是 Wall Clock ?

我们经常看到的系统时间,也就是现实中的实际时间,从UTC1970-1-1 0:0:0开始计时。这个时间可以改变,通过手工更改或者通过类似 NTP 服务来自动更改。很多软件会利用它来控制试用期是否结束,我们只要把系统时间调整一下提前一段时间,就又可以继续试用该软件。还有两个典型的例子:夏令时和“闰秒”,都可以通过调整这个时间来达到目的。

什么是 Monotonic Clock?

绝对时间,表示从之前某个时间点开始的逝去时间。它是无法更改的,不受 Wall Clock 时间调整的影响,所以天然适合用作计算事件发生的时间间隔。也可以理解为从开机到当下的时间间隔,与Linux 的系统命令 uptime 类似,利用 jiffies 计数。

如何计算时间间隔?

简单来说,记录下某个时间点作为开始时间,然后开始运行事件,再记录下结束时间,然后再使用结束时间减去开始时间,从而得到事件运行的时间间隔。用代码可以表述为:

  1. start := time.Now()
  2. ... 事件运行了 20 milliseconds ...
  3. t := time.Now()
  4. elapsed := t.Sub(start)

二零一七年 DNS 事件

在二零一七年新年之夜,国外 CDN 服务提供商 Cloudflare 的 DNS 出现大规模故障,导致很多网站无法正常被访问。经过工程师的代码追踪(相关功能使用 Go 实现),发现了问题的根本原因,Go 的函数 time.Now().Sub 对时间的度量仅使用了Wall Clock,而没有使用 Monotonic Clock,由于“额外的闰秒”以至于返回负值。“额外的闰秒”,是新年夜在全时间范围内添加的闰秒(leap second)。目前世界上经常通过补充额外的秒数,来解决闰秒的问题。
操作系统通常会在午夜到来前将时间倒退 1 秒,所以晚上 11:59 分 59 秒会出现两次。

  1. start := time.Now() // 11:59:59.990 开始
  2. ... 事件运行了 20 milliseconds ...
  3. t := time.Now() // 11:59:59.010 结束
  4. elapsed := t.Sub(start)

这样,时间间隔变成了-980 milliseconds。

问题的解决

Go 1.9 (released August 24, 2017), 核心库做了更新,增加了对 Monotonic Clock 的支持。我们可以看看相关 Time 结构的变化,

  1. type Time struct {
  2. // wall and ext encode the wall time seconds, wall time nanoseconds,
  3. // and optional monotonic clock reading in nanoseconds.
  4. //
  5. // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
  6. // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
  7. // The nanoseconds field is in the range [0, 999999999].
  8. // If the hasMonotonic bit is 0, then the 33-bit field must be zero
  9. // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
  10. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
  11. // unsigned wall seconds since Jan 1 year 1885, and ext holds a
  12. // signed 64-bit monotonic clock reading, nanoseconds since process start.
  13. wall uint64
  14. ext int64
  15. loc *Location
  16. }

这样的变化对上层使用者来说,是透明的,非常友好。

实际上,Go 里面还有一种方法可以来获取 Monotonic Clock,该方法来自于 runtime 包。可以说在 Go 1.8 及其以前,支持 Monotonic Clock 的方法不在 time 包中,而在 runtime 包中。

  1. func nanotime() int64

代码如下:

  1. start := runtime.nanotime()
  2. ... 事件运行了 20 milliseconds ...
  3. end := runtime.nanotime()
  4. elapsed := end - start

目前仍旧有很多开源库在使用这个方法,即使 Go 已经到了 1.14。

任何一个强大的库,任何一门强大的语言,都是一步一步发展来的。正应了那句话,不经历 defect,怎变得强大?经常听到某某新出的库,新出的框架多么多么好,多么多么强大,在我看来,不经过时间的考验,不经过业务场景的洗礼,很难称其为:“好”和“强大”。新出的东西确实有很新颖的设计理念,总是能针对旧的设计缺陷,做出改进和提高,但它缺的是“时间”。

参考链接: