2038 年问题,讲清楚
· 约 1 分钟
把时间存为有符号 32 位秒数的系统会在 2038 年 1 月耗尽范围并回卷到 1901 年 12 月。现代主流平台多年前已经解决,如今的风险集中在嵌入式设备、遗留系统和序列化数据里。
2038 年 1 月 19 日 03:14:07 UTC,自 1970 年 1 月 1 日起累计的秒数将达到 2,147,483,647。这是有符号 32 位整数能容纳的最大值。再过一秒它回卷为 −2,147,483,648,那些系统会把它读成 1901 年 12 月 13 日。
这是一个日期确凿的真实问题,这一点在软件问题里相当少见。
为什么是 32 位
Unix 时间设计于二十世纪七十年代初,当时 32 位整数是自然的字长,六十八年的余量看着相当宽裕。那些系统上的 time_t 类型是有符号 32 位整数,这一选择随后蔓延进 C 语言、进每一种包裹 C 的语言、进文件格式、进网络协议、进数据库结构。
让溢出呈现出这种特征症状的正是符号位。无符号的 32 位计数能撑到 2106 年;有符号的把取值范围劈在 1970 年两侧,于是溢出落在 1901 年而不是零点。
已经发生过什么
2038 年问题在生产环境里出故障已经好些年了,因为软件本来就经常计算将来的日期。
- 三十年期房贷的计算从 2008 年起开始跨过这条边界。
- 证书有效期、缓存存活时间和定时任务反复撞上它。
- 2006 年,AOLserver 默认数据库超时里的一处缺陷——被设成十亿秒,在出事之前一直没事——在算出的到期时刻溢出时让整个软件停摆。
规律是:故障远早于那个日期到来,出现在看得最远的那段代码里。
已经修好了什么
情况远比恐慌所暗示的要好。
- 64 位系统:几乎所有 64 位平台上
time_t都是 64 位,大约二千九百二十亿年后才会用尽。 - 32 位上的 Linux:内核 5.6(2020 年 3 月)在 32 位架构上引入了 64 位
time_t,主要发行版也随之发布了配套的用户空间。 - JavaScript:以双精度浮点数表示毫秒,在 1970 年两侧各约二十七万五千年内有效,从来没有这个问题。
- Java:
Instant使用 64 位秒数加纳秒。 - Python:整数是任意精度的,约束来自平台的 C 库。
- PostgreSQL、MySQL 8.0.28 及以上、SQL Server:时间戳以 64 位存储。
风险还留在哪里
嵌入式系统。 工业控制器、医疗设备、车载电子、计量表和传感器,许多按二三十年的服役期设计,许多没有升级通道,许多早已装在现场。真正风险的大头在这里,而且没有一份集中的清单。
8.0.28 之前 MySQL 的 TIMESTAMP 类型。 它是 32 位秒数,文档写明的上限就是 2038-01-19 03:14:07。DATETIME 从未受影响。大量生产数据正躺在那个受影响的列类型里。
序列化数据与文件格式。 凡是写下过 32 位时间字段的,无论读取方多新,它依旧是 32 位时间字段。老旧的归档格式、二进制协议和磁盘上的结构会把这个约束一路带下去。
仍在服役的 32 位构建。 一些路由器、机顶盒和长寿命设备,跑的是未打补丁内核之上的 32 位用户空间。
该怎么办
清点存储类型。 找出用 int(11) 存时间戳的列、旧版本上的 MySQL TIMESTAMP,以及任何带 32 位时间字段的二进制格式。数据活得比代码久的地方就在这里。
用 2038 年之后的日期测试。 把一份测试夹具设到 2040 年再跑整套用例。只要有人肯试,绝大多数 2038 年缺陷都能轻易复现。
优先选 64 位整数或 ISO 8601 字符串。 存毫秒的 BIGINT 或者 TIMESTAMPTZ 列没有值得操心的期限。ISO 8601 字符串则完全没有,代价是体积和比较速度。
再也不要把日期存进任何 32 位的东西里。 在现代硬件上省下的存储毫无意义,而这条期限如今已经落在普通软件的服役期之内。
其他的时间期限
2038 年是最近的一个,不是唯一一个。
- 2036 年:NTP 的 32 位纪元字段在 2 月 7 日翻转。NTPv4 能应付,但各处部署参差不齐。
- 2106 年:无符号 32 位秒数回卷。
- 2262 年:64 位_纳秒_计数溢出——这正是 Go 语言
time.Time的纳秒表示,以及 pandas 默认的 datetime64[ns]。
对任何写数据处理代码的人来说,最后这一条比听上去要近,而且是同一个错误换了尺度:一个细到足够好用的单位,装进了当年看着宽裕的位宽里。
怎么检查自己的系统
四项检查,按投入从小到大排列。
看数据库的列类型。 在 PostgreSQL 里,SELECT column_name, data_type FROM information_schema.columns WHERE data_type LIKE '%timestamp%' 会告诉你手上有什么;timestamp with time zone 是 64 位的,安全。在 MySQL 里,找 8.0.28 之前服务器上的 TIMESTAMP 列——有风险的是它们,DATETIME 从来都不是。
把时钟拨到将来。 在一个用完即弃的容器里把系统日期设成 2040 年,再跑一遍测试。方法很粗,却能查出出人意料的一大堆问题,因为越过边界之后的 2038 年缺陷几乎没有一个是隐晦的。
搜出 32 位的时间类型。 在 C 和 C++ 里,指在 32 位目标上保存时间值的每一个显式 int 或 long;在序列化定义里,指每一个 4 字节的时间字段;在应用代码里,指每一处为了存储或传输而把时间戳压成 32 位整数的地方。
在夹具里测试遥远的将来,而不只是不远的将来。 用「此刻加一天」的测试会一直通过到 2038 年,然后在某个星期二失败。用明确的 2040 年日期的测试今天就失败——趁着还有人腾得出手来修。
不要做什么
不要靠改成无符号 32 位整数来「解决」它。那买来六十八年,却失去表示 1970 年以前任何日期的能力,并把同一场对话推给 2106 年维护这套系统的人。4 字节与 8 字节之差,不值得再迁移一次。
也不要以为现代语言会替你挡住。应用代码可以完全安全,而它读取的数据却出自某个并不安全的东西之手——文件格式和传输协议会把这个约束带过每一次重写。