| 导语 最近看书再次看到了墙上时钟与单调时钟,瞬间勾起了对 go time 关于这两种时钟的支持与使用。以下内容都来自对官方文档的解读与理解。
引言--时钟的重要性
在现代计算机领域中,时钟几乎无处不在,特别是当今分布式系统流行的今天许多场景更是依赖于时钟,例如:
- 请求是否超时
- 某项服务 p99 响应时间为多少
- 某个服务的 qps 是多少
- 缓存何时过期
- 分布式节点失联多长时间将其剔除
- 用户在浏览短视频时在某个视频停留时间
- 用户查看广告的时间
可以这么说,离开了时钟,几乎所有的服务都将无法正常工作。但是时钟却不是我们想象的那么可靠,比如几台机器使用 NTP 来同步时间,那么很可能一台机器的墙上时间走着走着忽然回退了。这些微妙的小问题,往往会引发一些未知的错误。接下来带你去看看 go time 包关于时钟的处理,首先来了解下墙上时钟与单调时钟。
墙上时钟(wall clock) 与单调时钟(monotonic clock)
墙上时钟
墙上时钟根据墙上时间返回当前的日期与事件。一般会返回自纪元1970年1月1日(UTC)以来的秒数和毫秒数,不含闰秒。但不是绝对的,有些系统会使用其他日期作为参考点(可参考:https://zh.wikipedia.org/wiki/系统时间)。
墙上时钟一般是要与 NTP 同步的,但是如果本地时钟要是远远快于 NTP 服务器,就会强行重置,导致出现上文说得时间会回退。所以墙上时钟不适合测量时间间隔。
单调时钟
单调时钟顾名思义是总是向前的。所以它适合用来测量时间间隔。例如统计请求处理花费的时间等。单调时钟是单节点的,所以比较不同节点上的单调时钟毫无意义。
GO 中时钟的设计
如果你是一个喜欢看 go 源码或者看 go 设计的,肯定会首先看一个包的包说明,go 中 time 包的说明也说得很明白了,它是在一个 time 包内同时提供了墙上时钟与单调时钟。验证也比较简单:
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Println(t.String())
}
运行以上代码你会得到类似以下输出(其中 m= 前半部分的输出就是墙上时间具体依赖于你的执行环境,而 m= 就是单调时钟,单位 s:所以 time.Now() 返回的时间是既包括墙上时间也包括单调时间,具体使用哪一个依赖于你使用的具体方法。
代码语言:javascript复制2009-11-10 23:00:00 0000 UTC m= 0.000000001
单调时钟与墙上时钟
- 不受墙上时钟影响的操作
比如经常使用的下面代码计算两个时间差,就是使用的单调时钟。下面的输出将固定为: 1s ,不会因为墙上时间的同步而调整。
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
time.Sleep(1 * time.Second)
end := time.Now()
fmt.Println(end.Sub(start))
}
还有一些像 time.Since(start), time.Until(deadline), and time.Now().Before(deadline) 也都不受墙上时钟影响。那么还有哪些规则呢?根据官方文档有如下几个方法:
2. t.Add 如果有单调时钟,则会同时将墙上时钟与单调时钟都做计算
3. t.AddDate(y,m,d), t.Round(d), t.Truncate(d) 以及 t.ln, t.Local 和 t.UTC 都会丢弃单调时钟而使用墙上时钟
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Printf("t:t%sn", t.String())
addT := t.Add(1 * time.Second)
fmt.Printf("add time:t%sn", addT.String())
addDateT := t.AddDate(0, 0, 1)
fmt.Printf("add date Time:t%sn", addDateT.String())
roundT := t.Round(10 * time.Microsecond)
fmt.Printf("round time:t%sn", roundT.String())
truncateT := t.Truncate(1 * time.Second)
fmt.Printf("truncate time:t%sn", truncateT.String())
localT := t.Local()
fmt.Printf("local time:t%sn", localT.String())
utcT := t.UTC()
fmt.Printf("utc time:t%sn", utcT.String())
fmt.Printf("add utcT:t%s", utcT.Add(1*time.Second).String())
}
运行以上代码你会得到类似下面的验证输出,其中最后一行输出验证了当使用 t.Add 时,如果没有单调时钟得到结果也不会包含单调时钟。
代码语言:javascript复制t: 2021-05-19 11:30:44.370092834 0800 CST m= 0.000077406
add time: 2021-05-19 11:30:45.370092834 0800 CST m= 1.000077406
add date Time: 2021-05-20 11:30:44.370092834 0800 CST
round time: 2021-05-19 11:30:44.37009 0800 CST
truncate time: 2021-05-19 11:30:44 0800 CST
local time: 2021-05-19 11:30:44.370092834 0800 CST
utc time: 2021-05-19 03:30:44.370092834 0000 UTC
add utcT: 2021-05-19 03:30:45.370092834 0000 UT
两个时间操作包含:t.After(u), t.Before(u), t.Equal(u) , t.Sub(u) 遵循以下规则:
t 和 u 都具有单调时钟,那么就会使用单调时钟而忽略墙上时钟,如果任何一个不具有单调时钟,则会使用墙上时钟。
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
fmt.Printf("t:t%sn", t.String())
u := t.Add(-1 * time.Second)
fmt.Printf("u:t%sn", u.String())
fmt.Printf("t.After(u):%tn", t.After(u))
u = t.AddDate(0, 0, -1)
fmt.Printf("u:t%sn", u.String())
fmt.Printf("t.After(u):%tn", t.After(u)
}
上面代码都会输出正确的结果:
代码语言:javascript复制t: 2021-05-19 14:38:36.251903117 0800 CST m= 0.000053631
u: 2021-05-19 14:38:35.251903117 0800 CST m=-0.999946369
t.After(u):true
u: 2021-05-18 14:38:36.251903117 0800 CST
t.After(u):tru
因为单调时钟只有在单节点甚至是只有在当前进程才有效的,所以 t.GobEncode, t.MarshalBinary, t.MarshalJSON, and t.MarshalText 都会丢弃单调时钟,而 t.Format 没有提供单调时钟解析。同样,构造函数time.Date,time.Parse,time.ParseInLocation和time.Unix,以及解组器t.GobDecode和t.UnmarshalBinary。 t.UnmarshalJSON和t.UnmarshalText始终创建没有单调时钟的时间。
但是请注意 == 操作符却是会比较 Location 和单调时钟的。
代码语言:javascript复制package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
u := t
fmt.Printf("t:t%s nu:t%s nt==u:t%tnn", t.String(), u.String(), t == u)
u = t.AddDate(0, 0, -1).AddDate(0, 0, 1)
fmt.Printf("t:t%s nu:t%s nt==u:t%tnn", t.String(), u.String(), t == u)
t = t.Local()
u = t.UTC()
fmt.Printf("t:t%s nu:t%s nt==u:t%tnn", t.String(), u.String(), t == u)
}
运行以上代码你会得到类似下面的输出,看到最后3行输出,虽然看着一样但是因为一个是 Local 时区,一个是UTC 时区,比较结果也是不同,感兴趣的可以读下源码
代码语言:javascript复制t: 2009-11-10 23:00:00 0000 UTC m= 0.000000001
u: 2009-11-10 23:00:00 0000 UTC m= 0.000000001
t==u: true
t: 2009-11-10 23:00:00 0000 UTC m= 0.000000001
u: 2009-11-10 23:00:00 0000 UTC
t==u: false
t: 2009-11-10 23:00:00 0000 UTC
u: 2009-11-10 23:00:00 0000 UTC
t==u: false
总结
虽然平时很少会注意到这样的细节,对于时钟来说往往是这样的细节导致一些微妙的 bug,比如一些操作有可能不会得到你想得到的结果,比如有些系统会因为系统休眠而停止单调时钟,那么当你使用 t.Sub(u) 时也许并不会得到你想要的结果。考虑在虚拟机中执行代码就有可能出现上述问题。
还有就是一直觉得 go 的源码文档是非常值得 go 学习者学习的,通过对 time 包的文档解读,更能够加深这点。 最后为了方便大家查阅复习,下面附上一个整理的表格:
func | 包含单调时钟 | 包含墙上时钟 | 备注 |
---|---|---|---|
time.Now() | y | y | |
t.Add() | t 包含结果就包含,否则就不包含 | y | |
t.AddDate(y, m, d), t.Round(d), and t.Truncate(d) | n(计算时会抛弃单调时钟) | y | |
t.In, t.Local, and t.UTC | n | y | |
t.After(u), t.Before(u), t.Equal(u), and t.Sub(u), t.Before(u) | 如果 t 与 u 都包含单调时钟,那么就用单调时钟计算,否则就用墙上时钟计算 | ||
t.GobEncode, t.MarshalBinary, t.MarshalJSON, and t.MarshalText | n | y | |
t.GobDecode, t.UnmarshalBinary. t.UnmarshalJSON, and t.UnmarshalText | n | y | |
t.Format | n | y | |
t == u | y | y | 不仅会比较单调时钟与墙上时钟,还会比较时区 |