后端 · 时间

只存 UTC 还不够:夏令时、闰秒和那次放倒半个互联网的跳秒

"时间都存 UTC 不就行了?"——这是工程师面对时间 bug 时的第一道护身符,也是个只能及格的答案。UTC 解决了"时区偏移",但解决不了夏令时那一小时凭空消失、也解决不了闰秒。2012 年 6 月 30 日那个闰秒,把 Reddit、LinkedIn 和一票跑 Java 的服务集体放倒,根因深到 Linux 内核里。这篇讲 UTC 之外的那些坑。

先把"存 UTC"这条铁律说清楚:它对,但它只覆盖了一半问题。把所有时间点以 UTC 时间戳存储、只在展示给用户的最后一刻转成本地时区——这是对的,照做。问题在于,有些时间根本不是"一个时间点",而是"墙上时钟的某个读数",这类需求 UTC 救不了。下面分三层,从最常见到最阴间。

第一层:未来的会议,不能存 UTC

这是最反直觉的一条。用户在北京时间 2027 年 3 月 8 日早上 9:00 定了个跨国周会。你顺手转成 UTC 存下来:2027-03-08 01:00:00Z。看起来很专业。

问题来了:万一在这之前,某个参会者所在的时区修改了夏令时规则呢?时区规则不是物理常数,它是各国政府说改就改的法律。欧盟讨论废除夏令时讨论了好几年,各国 DST 起止日期年年有微调。你存下的那个 UTC 时刻一旦规则变了,换算回当地的"上午 9 点"就错位了——会议会在错误的本地时间响铃。

💡

规则:过去发生的事件(日志、交易、下单时间)存 UTC 时间戳,没错。未来的"墙上时间"(日程、闹钟、定时任务)要存本地时间 + IANA 时区名(如 Asia/Shanghai),到点了再用最新的时区数据库换算。别存死 UTC,也别存 +08:00 这种固定偏移——偏移会随 DST 变,时区名才稳定。

第二层:夏令时,一年有一小时不存在、一小时存在两次

实行夏令时的地区,每年"春季前拨"那天,本地时钟从 02:00 直接跳到 03:00。02:00–02:59 这一小时在那天根本不存在。"秋季回拨"那天反过来,01:00–01:59 会走两遍——同一个本地时间出现两次,无法区分是第一次还是第二次。

这两件事在代码里就是真实的崩溃点:

// 美国 2027 DST 前拨日,本地 02:30 不存在
// 不同运行时给的答案不一样,别依赖这种边界行为
new Date("2027-03-14T02:30:00-05:00")  // 这是有效的——因为带了明确偏移
// 真正的坑是:只给本地"02:30"、让库去猜偏移时

排查这类问题,最有用的动作是把时间戳和本地时间双向对一遍,确认偏移对不对。直接丢进 在线时间戳转换器,秒级/毫秒级、UTC 和本地并排看,比在代码里 console.log 一堆 Date 高效。

第三层:闰秒,2012 年那次真的放倒了半个互联网

前两层是业务逻辑。闰秒是另一个维度的麻烦——它能直接打挂操作系统。

地球自转不均匀,UTC 偶尔要补一个"闰秒"对齐天文时间。2012 年 6 月 30 日午夜(UTC),全球时钟在 23:59:59 之后插入了一个 23:59:60,再到 00:00:00。就这一秒,引爆了一个潜伏多年的 Linux 内核 bug

机制是这样的:NTP 服务器提前置位"插入闰秒"标志,内核启动一个 hrtimer(高精度定时器)在月末午夜插秒。但插秒时,内核漏了调用 clock_was_set() 去通知 hrtimer 子系统时间变了。后果是所有 CLOCK_REALTIME 的高精度定时器以为自己早到期了——亚秒级的超时全部立即触发

🚫

凡是用 futex 做细粒度 pthread 超时的程序,会陷入"超时→重新挂起→又立刻超时"的空转死循环,单机 CPU 直接顶到接近 100%。点名中招的有 mysqld、Java(JVM)、Firefox、Thunderbird、VirtualBox。当晚 Reddit、LinkedIn、Mozilla 的 Java 服务(Mozilla Bug 769972)相继出问题,而且只影响那些在闰秒之前就已经在跑的进程。

应急绕过当年传疯了的一招,是强制重设一次系统时钟:

# 把当前时间再写回去一次,借 settimeofday() 触发 clock_was_set()
date -s "$(date)"
# 重启也行——重启后这个一次性的错误状态就没了

真正的修复由内核开发者 John Stultz 提交、Thomas Gleixner 在 2012 年 7 月合入,顺带还修了一个 xtime_lock 上的活锁。

最值得记的是 Google 的做法:它不在午夜硬插一秒,而是用 leap smear(闰秒涂抹)——把这一秒摊到当天的很多次微小调整里慢慢消化,时钟永远是连续单调的,于是从根上没碰到那个内核 bug。这后来成了大厂的标准姿势。

顺手说:别自己写时区换算

三层坑下来,结论很朴素:时间这东西,能调库就别手算。

2012 那次最深的教训其实不是"闰秒难",而是极少执行的代码路径等于没测过的代码路径。闰秒几年才一次,那段内核逻辑就一直带病运行到某个午夜集体爆发。你代码里那些"几乎不会走到"的时间分支,多半也是同一种债。

在线时间戳转换器

Unix 时间戳 ↔ 日期,秒级毫秒级、UTC 与本地时区并排对照,全本地计算

打开时间戳转换器