最近在写关于 Date/Time 的代码,不由得想起一件发生在二零一七年的往事,重温一下 Wall Clock 和 Monotonic Clock。
什么是 Wall Clock ?
我们经常看到的系统时间,也就是现实中的实际时间,从UTC1970-1-1 0:0:0开始计时。这个时间可以改变,通过手工更改或者通过类似 NTP 服务来自动更改。很多软件会利用它来控制试用期是否结束,我们只要把系统时间调整一下提前一段时间,就又可以继续试用该软件。还有两个典型的例子:夏令时和“闰秒”,都可以通过调整这个时间来达到目的。
什么是 Monotonic Clock?
绝对时间,表示从之前某个时间点开始的逝去时间。它是无法更改的,不受 Wall Clock 时间调整的影响,所以天然适合用作计算事件发生的时间间隔。也可以理解为从开机到当下的时间间隔,与Linux 的系统命令 uptime 类似,利用 jiffies 计数。
如何计算时间间隔?
简单来说,记录下某个时间点作为开始时间,然后开始运行事件,再记录下结束时间,然后再使用结束时间减去开始时间,从而得到事件运行的时间间隔。用代码可以表述为:
start := time.Now()
... 事件运行了 20 milliseconds ...
t := time.Now()
elapsed := t.Sub(start)
二零一七年 DNS 事件
在二零一七年新年之夜,国外 CDN 服务提供商 Cloudflare 的 DNS 出现大规模故障,导致很多网站无法正常被访问。经过工程师的代码追踪(相关功能使用 Go 实现),发现了问题的根本原因,Go 的函数 time.Now().Sub 对时间的度量仅使用了Wall Clock,而没有使用 Monotonic Clock,由于“额外的闰秒”以至于返回负值。“额外的闰秒”,是新年夜在全时间范围内添加的闰秒(leap second)。目前世界上经常通过补充额外的秒数,来解决闰秒的问题。
操作系统通常会在午夜到来前将时间倒退 1 秒,所以晚上 11:59 分 59 秒会出现两次。
start := time.Now() // 11:59:59.990 开始
... 事件运行了 20 milliseconds ...
t := time.Now() // 11:59:59.010 结束
elapsed := t.Sub(start)
这样,时间间隔变成了-980 milliseconds。
问题的解决
Go 1.9 (released August 24, 2017), 核心库做了更新,增加了对 Monotonic Clock 的支持。我们可以看看相关 Time 结构的变化,
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
loc *Location
}
这样的变化对上层使用者来说,是透明的,非常友好。
实际上,Go 里面还有一种方法可以来获取 Monotonic Clock,该方法来自于 runtime 包。可以说在 Go 1.8 及其以前,支持 Monotonic Clock 的方法不在 time 包中,而在 runtime 包中。
func nanotime() int64
代码如下:
start := runtime.nanotime()
... 事件运行了 20 milliseconds ...
end := runtime.nanotime()
elapsed := end - start
目前仍旧有很多开源库在使用这个方法,即使 Go 已经到了 1.14。
任何一个强大的库,任何一门强大的语言,都是一步一步发展来的。正应了那句话,不经历 defect,怎变得强大?经常听到某某新出的库,新出的框架多么多么好,多么多么强大,在我看来,不经过时间的考验,不经过业务场景的洗礼,很难称其为:“好”和“强大”。新出的东西确实有很新颖的设计理念,总是能针对旧的设计缺陷,做出改进和提高,但它缺的是“时间”。
参考链接: