2038 年问题(Y2K38)详解
2038 年 1 月 19 日 03:14:07 UTC,Unix 时间戳 2,147,483,647 —— 有符号 32 位整数的最大值 —— 将溢出并回绕到 1901 年的日期。本文讲解这意味着什么、哪些系统受影响、64 位 time_t 修复如何工作,以及如何测试你自己的代码。
一行说清溢出
有符号 32 位整数最大到 2,147,483,647。存为 Unix 秒时,该值是 2038-01-19 03:14:07 UTC——一秒后回绕到 -2,147,483,648,被读作 1901 年 12 月 13 日。任何仍把时间保存在有符号 32 位整数里的系统,都会在那一刻跳回 137 年前。修复就一个词:64 位。下表显示哪些存储类型受影响、哪些已经安全。
| 存储类型 | 溢出于 / 最大日期 | 2038 年后安全? |
|---|---|---|
| 有符号 32 位 time_t / SQL INT | 2038-01-19 03:14:07 UTC | 否 |
| MySQL TIMESTAMP | 2038-01-19 03:14:07 UTC | 否 —— 用 DATETIME |
| 无符号 32 位 | 2106-02-07 06:28:15 UTC | 多争取 68 年,非解决办法 |
| 有符号 64 位 time_t / SQL BIGINT | 约 2920 亿年 | 是 |
| 64 位浮点(JavaScript、Python) | 公元 275,760 年 | 是 |
| MySQL DATETIME(打包字段) | 公元 9999 年 | 是 |
什么是 2038 年问题?
Unix 时间戳传统上存为一个有符号 32 位整数,计自 1970 年 1 月 1 日 00:00:00 UTC 起的秒数。有符号 32 位整数的最大值是 2,147,483,647——对应 2038 年 1 月 19 日 03:14:07 UTC。那一刻之后一秒,整数溢出到 -2,147,483,648。作为 Unix 时间戳解释时,这个负值对应 1901 年 12 月 13 日——把受影响的系统送回近 137 年前。
- 2^31 − 1 = 2,147,483,647(有符号 32 位整数最大值)
- 作为日期的 2,147,483,647 = 2038 年 1 月 19 日 03:14:07 UTC
- 2,147,483,647 + 1 溢出到 −2,147,483,648(最小的 32 位值)
- 作为日期的 −2,147,483,648 = 1901 年 12 月 13 日 20:45:52 UTC
- 溢出的当地时间各不相同:1 月 18 日 22:14:07 EST,1 月 19 日 14:14:07 JST
哪些系统有风险?
该问题影响把时间戳存为有符号 32 位整数的系统:
- 使用 32 位 time_t 的嵌入式系统和单片机
- 为 32 位目标编译、用 time_t 或 int 存时间戳的老旧 C/C++ 代码
- 仍在生产中的 32 位硬件上的老旧 Linux 内核(5.6 之前)
- MySQL TIMESTAMP 列——限制到 2038-01-19;改用 DATETIME
- 一些把时间戳编码为 32 位值的网络协议和文件格式
- 寿命长达数十年的工业控制系统和 IoT 设备中的固件
哪些系统是安全的?
大多数现代代码和基础设施已不受影响:
- 任何 64 位操作系统——Linux(glibc 2.34+ 也修复 32 位硬件)、macOS、Windows
- Python——时间戳使用 64 位浮点,远超公元 2.92 亿年仍安全
- JavaScript——Date 使用 64 位 IEEE 754 浮点,安全到公元 275,760 年
- Go 和 Rust——内部用 int64 表示时间,安全数十亿年
- Java——java.time.Instant 用 long(64 位),安全到约公元 2.92 亿年
- PostgreSQL TIMESTAMP——正确存储 2038 年以后的日期
- MySQL DATETIME——范围 1000-01-01 到 9999-12-31,不受影响
MySQL TIMESTAMP 与 2038 年上限
MySQL 有两种主要的日期时间列类型,在 2038 年前后表现迥异:
- TIMESTAMP 在内部存为有符号 32 位 Unix 整数——于 2038-01-19 03:14:07 UTC 溢出
- DATETIME 存为打包的 年/月/日 时/分/秒 字段,支持到公元 9999 年——任何可能晚于 2038 的日期都用它
- TIMESTAMP 还会隐式在服务器会话时区之间转换;DATETIME 按原样存储该值
- 对于原始 epoch 列,存 BIGINT 而非 INT,使该值本身不会溢出
- 审计现有架构:SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE DATA_TYPE = "timestamp"
嵌入式 Linux 与 32 位 time_t
剩下最大的 Y2038 风险在于长寿命的嵌入式系统:路由器、工业控制器、汽车 ECU、智能电表,以及以 32 位内核出厂、可能在 2038 年后仍服役的 IoT 设备。C 库和任何自定义二进制协议都必须是 64 位干净的。
- glibc 通过构建宏 -D_TIME_BITS=64 为 32 位 ABI 获得 64 位 time_t(glibc 2.34+)
- musl libc 自 1.2.0 版(2020)起默认 64 位 time_t
- Debian、Ubuntu 和 OpenWrt 都已把 32 位移植迁移到 64 位 time_t
- 文件系统时间戳是另一个问题:ext2/ext3 用 32 位时间;ext4 在 ≥ 256 字节的 inode 上支持 64 位
- 对 2026 年后仍出货的硬件,确认 C 库与任何线缆格式都是 64 位
如何修复并测试 Y2038
修复始终相同——把时间拓宽到 64 位——但必须在每一个存储或传输时间戳的层级应用:C 库、数据库列,以及任何二进制协议或文件格式。最快找出问题的办法,是在测试环境把时钟拨过该边界,盯住落到 1901 年的日期。
- 在 glibc 2.34+ 上用 -D_TIME_BITS=64(以及 -D_FILE_OFFSET_BITS=64)编译 32 位代码
- 对任何存储原始 Unix 秒或毫秒的列,使用 BIGINT 而非 INT
- 对可能超过 2038 的日期,优先用 DATETIME 或 TIMESTAMPTZ,而非 MySQL TIMESTAMP
- 测试时,把系统或容器时钟设为 2038-01-19T03:14:08Z,并断言日期仍在 2038 年
- 检查线缆格式和文件头——如果协议字段是 32 位,64 位操作系统也救不了
相关翻转:Y2K、GPS、NTP 与 2106
2038 年是一族定宽翻转中的一个。每一个都源于把时钟塞进太少的位里,知道这些日期有助于在下一个上线前发现它:
| 翻转 | 日期 | 原因 |
|---|---|---|
| Y2K | 2000-01-01 | 两位数年份存储 |
| GPS 周翻转 | 2019-04-06 | 10 位 GPS 周计数器 |
| NTP 纪元翻转 | 2036-02-07 | 自 1900 年起的无符号 32 位秒 |
| 2038 年(Y2K38) | 2038-01-19 | 自 1970 年起的有符号 32 位秒 |
| 2106 年 | 2106-02-07 | 自 1970 年起的无符号 32 位秒 |
相关参考
FAQ
- 什么是 2038 年问题?
- 2038 年问题(Y2K38)是一个 bug:用有符号 32 位整数存储 Unix 时间戳的系统将在 2038 年 1 月 19 日 03:14:07 UTC 溢出。有符号 32 位整数的最大值是 2,147,483,647。一秒后,整数回绕到 -2,147,483,648,对应 1901 年 12 月 13 日。
- 2038 年 1 月 19 日的 Unix 时间戳是多少?
- 2038 年 1 月 19 日 03:14:07 UTC 的 Unix 时间戳是 2,147,483,647 —— 有符号 32 位整数的最大值(2^31 - 1)。这是 32 位系统上发生 2038 年溢出的确切时刻。
- 如何修复 2038 年问题?
- 用有符号 64 位整数而非 32 位来存储时间。使用 64 位 time_t(在 glibc 2.34+ 上用 -D_TIME_BITS=64 编译 32 位代码),epoch 列用 BIGINT 而非 INT,用 DATETIME 而非 MySQL TIMESTAMP。有符号 64 位时间值约 2920 亿年都不会溢出。
- Y2K38 和 Y2K 一样吗?
- 不。Y2K(2000 年问题)源于程序把年份存成两位数。Y2K38 源于把 Unix 时间戳存为有符号 32 位整数,在 2038 年溢出。成因不同,受影响的系统也不同。
- 什么是 2106 年问题?
- 2106 年是无符号 32 位时间戳的同一种溢出,于 2106 年 2 月 7 日 06:28:15 UTC 耗尽。从有符号改为无符号 32 位多争取 68 年,但不是真正的解决办法;64 位时间才是。
- 2038 年互联网会崩溃吗?
- 大概率不会有重大崩溃。大多数互联网基础设施已迁移到 64 位系统。风险集中在嵌入式系统、老旧 IoT 设备,以及数十年未更新的软件。现代操作系统、编程语言和数据库都已经安全。