mzh/blog

程序中的时间问题

leap second

最近Russ Cox提了一条proposal《Monotonic Elapsed Time Measurements in Go》

这个proposal主要解决了一个问题,计算时间差。 我一开始觉得,这不是很简单么?例如:

t1 := time.Now()
# ... 10 ms of work
t2 := time.Now()
const f = "15:04:05.000"
fmt.Println(t1.Format(f), t2.Sub(t1), t2.Format(f))

简单的相减,就可以得到时间差。但这里隐含了一个谬误,就是:

默认:程序后执行的Now,肯定比之前执行的Now晚。

时间,在人的认知里肯定是单向向前,但在计算机系统里,却不是。因为有正、负闰秒的存在,常常有bug出现,例如:Cloudflare RRDNS 故障

这起事故中,CF的RRDNS使用两个时间的差值进行权重计算,平时是没有问题,但当闰秒的出现时,程序差值就出现了负数,最终酿成了事故。所以在对时间差有严重依赖的程序(纳入计算权重等),需要使用的是monotonic time(单调递增时间),而不是wall time(系统时间),这也就促成了Russ Cox的proposal。

不但如此,程序员还有对于时区、时差、时间精度、起始的谬误。因此,就有人总结出了《程序员认知时间的谬误(Falsehoods programmers believe about time)》 ,我摘抄了部分国内程序员常犯的错误,括号中是我个人对于这些谬误的理解:

  1. 一天总有24小时
  2. 一个月有30或者31天(还有二月份)
  3. 一年有365天 (闰秒导致多一秒,1582年整整消失了10天
  4. 二月份总是28天(有29天)
  5. 24小时是一天、周、月的周期值(对时间取模算天数、我也犯过)
  6. 每年相同月份里,周的起始是相同的
  7. 机器的时区永远是GMT,不,开玩笑的,是机器的时区永远不变(时区的设置可能会被调整)
  8. 系统时钟永远设置的是本地对应的时区的时间
  9. 目标时区和GMT永远有相同的间隔时间(时区会变化,夏令时)
  10. 客户端的时间和服务器时间永远相同
  11. 客户端时间和服务器时间的差值没什么大不了的
  12. 就算CS有差值,总是相同的间隔(客户端可能会动态调整时间)
  13. 服务器和客户端的时间永远在同一个时区
  14. 系统时间不会在5000年前、或者5000年后
  15. 时间没有起点和尽头(千年虫、Unix epoch)
  16. 系统中的一分钟和其他机器上的一分钟应该是一样的(各个机器上CPU频率等问题导致时钟偏移,所以请使用ntp)
  17. 最小的时间单位是秒、呃、毫秒……(最小单位是CPU确定的)
  18. 大家都能明白时间戳格式(1339972628 或者是 133997262837)
  19. 时间戳格式永远是同一个格式(64位和32位就有区别)
  20. 时间戳精度永远相同(float精度问题)
  21. 时间戳的精度保证了可以做uid
  22. 全世界都能明白11/07/05是什么时间格式(年月日显示格式不明确)

我的经历中,见得比较多的是对于时区了解的匮乏,很多程序员并不知道夏令时,甚至有产品或者老板的需求就只是给国人服务,当要全球化时就抓瞎了。当然,最多的还是对闰秒的无视,或者有人根本不知道闰秒的存在。不过,这些bug都因为系统对时间依赖程度不高,或因为系统跑的时间不够长,(没长到碰到闰秒就下线了),所以并没有导致算错帐或者生产事故。不过,这些理由都不能为这种bug开脱,所以程序员要学习东西还是很多的。

p.s. 你在生产系统里碰到过什么关于时间的bug?欢迎讨论~