- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 谎言清单这一文体
- 谎言 1 — 「一天有 24 小时」
- 谎言 2 — 「一分钟有 60 秒」
- 谎言 3 — 「时钟只会向前走」
- 谎言 4 — 「时区是固定的偏移量」
- 其他谎言 — 简要一览
- 为什么我们会一再上当
- 不被谎言欺骗的习惯
- 结语
- 参考资料
引言 — 谎言清单这一文体
软件开发领域有一个著名的文章体裁,叫做「程序员对 X 深信不疑的谎言」。关于姓名的谎言、关于地址的谎言,而其中最臭名昭著的,就是关于时间的谎言。形式很简单:把我们认为「理所当然」的命题一一列出,然后逐条证明它们其实都是错的。
这一体裁之所以流行,是因为这些命题实在太「理所当然」了。「一天有 24 小时」——谁会对这句话提出异议呢?可正是这份理所当然,才是陷阱所在。我们把这类假设毫不怀疑地刻进代码里,这些代码在大多数情况下都运行良好,直到某个例外的瞬间,悄无声息地崩溃。
本文从关于时间的谎言中,挑出四条最常咬伤开发者的,分别附上一个真实发生过的 bug 来说明。写这篇文章的目的不是吓唬读者,而是让你自己检查一下:「我的代码里是不是也藏着这样的假设?」如果需要处理时间的工具,可以用本站的世界时钟,把多个地区的时刻并排比较。
谎言 1 — 「一天有 24 小时」
这是最根本的谎言。「一天恰好是 24 小时,也就是 86,400 秒」这个假设,在好几种情况下都会出错。
因为夏令时。 在夏令时开始的春季某一天,时钟会向前跳一小时,那一天就是 23 小时的一天。反过来,夏令时结束的秋季某一天,则是 25 小时的一天。所以如果无条件地把「从今天午夜到明天午夜」算作 86,400 秒,一年会错两次。
除此之外,在一个国家变更标准时本身、或跨越国际日期变更线这类特殊情况下,一天的长度甚至会发生更剧烈的变化。实际上,有些地区就曾因为政策变更,把某个特定日期整个跳过。
真实案例。 某考勤管理系统把工作时长算作「上班时刻与下班时刻之差」。可是在夏令时结束、变成 25 小时的那一天,通宵值班员工的工作时长被多算了一小时;反过来,在夏令时开始那天,则被少算了一小时。「一天等于 24 小时,两个时刻的简单相减等于经过时间」这个假设,在夏令时面前崩溃了。正确的做法是把两个时刻都转换成 UTC,再计算两者之差——UTC 没有夏令时,所以经过时间是准确的。
谎言 2 — 「一分钟有 60 秒」
这一条也几乎永远成立,但并非永远成立——原因是前面提到的闰秒(leap second)。
地球自转的速度存在微小的不规律,因此以原子钟为基准的时间,和以地球自转为基准的时间之间会不断积累误差。为了修正这一误差,国际机构会不时在某一天的末尾多插入 1 秒。那一天的最后一分钟,就不是 60 秒,而是 61 秒。
插入闰秒的那一天:
23:59:59
23:59:60 ← 正常存在的时刻
00:00:00
大多数应用程序都不需要在意这一点。但在需要极其精确地处理时刻、或严格进行秒级运算的系统中,这就会成为问题。
真实案例。 过去在插入闰秒的那一刻,曾发生过一起多个大型服务同时出现故障的著名事件。部分系统的内核与应用程序,在遇到「23:59:60」这个意料之外的值时,陷入了无限循环,或是把 CPU 烧到 100% 后卡死。「秒数范围是 0 到 59」这一假设,被深深埋进了代码之中。这起事件之后,许多大型基础设施引入了 "leap smearing" 方式,把闰秒分摊到一整天里一点点吸收——这是一种彻底不向系统暴露 61 秒长的一分钟的策略。
谎言 3 — 「时钟只会向前走」
时间只会单调向前流动,这是一种物理直觉,但并不适用于计算机的时钟。计算机的挂钟(wall clock)是可以倒退的。
最常见的原因是 NTP(Network Time Protocol)同步。计算机的本地时钟会一点点产生偏差,NTP 会把它校正到准确的时刻。如果本地时钟比实际时间超前,NTP 就会把时钟往回调——那一刻,「现在」反而比刚才更早。夏令时结束(秋季把时钟拨回一小时)同样会把当地时刻倒回去。
真实案例。 某个服务曾这样测量请求的处理时间。
start = 当前_挂钟_时间()
... 处理请求 ...
duration = 当前_挂钟_时间() - start
平时运行得很好。但恰好在处理过程中,NTP 把时钟往回调整时,duration 就变成了负数。这个负值又流入了超时计算和指标聚合,导致一些请求立即超时,一些统计数据变得毫无意义。定位原因花了好几天,因为几乎无法复现。教训很明确:测量经过时间时,必须使用绝不会倒退的单调时钟(monotonic clock)。原则是,挂钟只用来知道「现在几点」,而「经过了多长时间」要用单调时钟来测量。
谎言 4 — 「时区是固定的偏移量」
我们很容易以为 Asia/Seoul 永远是 UTC+9,America/New_York 永远是 UTC-5。但时区并不是固定的偏移量,而是一套随时间变化的规则集合。
最大的原因还是夏令时。有夏令时的地区,一年会有两次偏移量变化——纽约冬季是 UTC-5,夏季是 UTC-4。而且各国还会出于政治、经济原因,实际调整时间政策:新引入或废止夏令时,甚至变更标准时的偏移量本身。这类变更发生的频率比想象中更高,有时只提前几周做个简短预告。而 IANA 时区数据库,收录了所有这些变更的历史记录。
真实案例。 某个全球日历应用在保存会议时刻时,没有保存时区名称,只保存了那一刻的偏移量。当用户把会议安排在「3 个月后上午 10 点(纽约)」时,应用把当时的偏移量 UTC-4 一并固化保存了下来。可是在这 3 个月之间,夏令时结束、纽约变成了 UTC-5,于是被保存的会议在实际挂钟时刻上显示为上午 9 点。结果用户们提前一小时出现在了会议现场。
正确的做法是,保存未来事件时使用时区名称(America/New_York),而不是偏移量。这样一来,无论夏令时规则如何变化,显示时都会被重新计算为正确的挂钟时刻。
其他谎言 — 简要一览
除了上面四条之外,「关于时间的谎言」清单里还有很多有名的条目。这里简单列举几条。
- 「一个国家只使用一个时区。」 不对。像美国、俄罗斯、澳大利亚这样拥有多个时区的国家不在少数。反过来,也有像中国这样国土辽阔却只用单一时区的国家。
- 「时区偏移量以整小时为单位。」 不对。印度是 UTC+5:30、尼泊尔是 UTC+5:45,存在以 30 分钟、45 分钟为单位的偏移量。把偏移量假设为整数小时的代码,在这些地区就会出问题。
- 「比较两个时间戳就能知道时间先后。」 在分布式系统中不成立。如果不同服务器的时钟存在偏差,后发生的事件,其时间戳反而可能更早。
- 「闰年每 4 年出现一次。」 大体正确,但有例外。能被 100 整除的年份不是闰年,而其中能被 400 整除的年份又重新是闰年。2000 年是闰年,1900 年却不是。
- 「午夜永远存在。」 不对。部分地区曾因夏令时切换,导致某个日期的 00:00 并不存在。
这份清单的共同点是,它们全都是「大多数情况下成立,但存在例外」的命题。而软件 bug,永远诞生于那些例外之中。
为什么我们会一再上当
到这里,不妨问一个根本性的问题:为什么开发者会一次又一次地被这些谎言欺骗?原因有以下几点。
第一,这些假设符合日常经验。我们大多数人都生活在没有夏令时、或者即便有夏令时也不太在意的环境里,所以「一天可能不是 24 小时」这件事有违直觉。
第二,这些 bug 平时隐藏得很好。夏令时切换一年只有两次,闰秒几年才有一次,NTP 的时钟回拨也难以预测。所以它们很少在测试中暴露出来,只会在生产环境的某个特定瞬间才会爆发。难以复现,也就意味着难以调试。
第三,语言和库放任了这个陷阱。许多语言的默认日期类型,很容易允许没有时区信息的 naive 形式,也不会清楚地区分挂钟时间和单调时钟。所以错误的选择很容易成为默认值。
这三个原因叠加在一起,让时间类 bug 具备了「常被引入、却很少触发、难以捕捉」这种特有的性质。
不被谎言欺骗的习惯
光靠背诵这份清单是不够的,必须把它转化为实务中用来避开这些陷阱的习惯。
- 存储用 UTC。 用一个不受夏令时和偏移量变更影响的单一基准来存储,可以减少对「一天=24 小时」或「固定偏移量」之类假设的依赖。
- 经过时间用单调时钟测量。 这是对「时钟只会向前走」这一谎言的防御。
- 未来事件与时区名称一起保存。 这是对「时区是固定偏移量」这一谎言的防御。
- 不要硬编码时区规则,使用 IANA 数据库。 并且要保持更新。
- 在测试中加入极端情况。 特意去测试夏令时切换日、30 分钟/45 分钟偏移量的地区、闰年边界,以及国际日期变更线附近的情况。
- 始终追问「这个值究竟是绝对时刻,还是日历上的日期?」 这一个问题,就能防住许多类型选择上的失误。
结语
「程序员对时间深信不疑的谎言」这份清单之所以能长久流传,是因为它不是单纯的冷知识,而是一张真实 bug 的地图。「一天有 24 小时」也好,「一分钟有 60 秒」也好,「时钟只会向前走」也好,「时区是固定偏移量」也好,全都只是「大多数情况下成立」,而不是「永远成立」。
关键不在于把这些谎言一条条背下来,而在于理解它们背后的原理。时间不是纯粹的物理量,而是层层叠加的人类约定,这些约定因地区而异,也会随时间推移而改变。接受这一事实,养成 UTC 存储、单调时钟、IANA 数据库这类防御性习惯之后,这些谎言中的大多数就再也咬不到你的代码了。如果你想亲自比较多个地区的时刻,可以打开世界时钟,看看这些谎言到底是怎么显现出来的。
参考资料
- Falsehoods programmers believe about time: https://gist.github.com/timvisee/fcda9bbdff88d45cc9061606b4b923ca
- More falsehoods programmers believe about time: https://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
- IANA Time Zone Database: https://www.iana.org/time-zones
- Leap second: https://en.wikipedia.org/wiki/Leap_second
- Monotonic clock (Wikipedia): https://en.wikipedia.org/wiki/Monotonic_function
- The Problem with Time & Timezones (Computerphile): https://www.youtube.com/watch?v=-5wpm-gesOY