Year 2038 Problem

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 INT2038-01-19 03:14:07 UTC
MySQL TIMESTAMP2038-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 年是一族定寬翻轉中的一個。每一個都源於把時鐘塞進太少的位元裡,知道這些日期有助於在下一個上線前發現它:

翻轉日期原因
Y2K2000-01-01兩位數年份儲存
GPS 週翻轉2019-04-0610 位元 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 裝置,以及數十年未更新的軟體。現代作業系統、程式語言和資料庫都已經安全。