一次半夜的 NTP 校時,讓我的訂單編號撞號了
System Design·2026年6月26日·5 分鐘閱讀

一次半夜的 NTP 校時,讓我的訂單編號撞號了

L
Leo Wu

後端工程師,做過交易所與金流的高併發系統,被一次 NTP 校時教育過什麼叫「不要相信時鐘」。

凌晨三點多,值班手機響了。對帳服務噴出一堆「主鍵衝突」的錯誤——有兩筆完全不同的訂單,拿到了一模一樣的訂單編號。我們的訂單 ID 是用 Snowflake 演算法自己產的,理論上全域唯一、絕不重複。結果它重複了。這篇覆盤那晚到底發生什麼事,以及「時鐘」這個我們平常最不會去懷疑的東西,怎麼一口氣把一套設計良好的分散式 ID 打穿。

先簡單說 Snowflake 怎麼保證唯一

一次半夜的 NTP 校時,讓我的訂單編號撞號了:本文架構

Snowflake 產生的是一個 64 位元的整數,拆成幾段:

  • 一段是時間戳記(毫秒),佔大部分位元
  • 一段是機器 ID,區分是哪台機器產的
  • 一段是序號,同一台機器、同一毫秒內遞增

它保證唯一的邏輯是這樣:不同機器靠機器 ID 區分;同一台機器、同一毫秒內靠序號區分;跨毫秒,靠時間戳記往前走來區分。

看出來了嗎?整個唯一性的地基,是「時間只會往前走」這個假設。序號在同一毫秒內用完會等到下一毫秒,而「下一毫秒」預設時間戳記一定比現在大。一旦這個假設破了,整套就垮了。

Snowflake ID 的 64 位元結構:時間戳記、機器 ID、序號三段組成
Snowflake ID 的 64 位元結構:時間戳記、機器 ID、序號三段組成

那晚時間往回跳了

那台機器的系統時間,因為 NTP 校時,往回跳了大概幾十毫秒。

平常我們巴不得伺服器時間準,所以都開 NTP 自動校時。但 NTP 在發現本地時間比標準時間快的時候,有時候會直接把時鐘往回撥。對一般服務這沒什麼,對 Snowflake 這種「拿時間當地基」的演算法,這是災難。

時間一往回跳,新產生的 ID 用的時間戳記,就可能跟幾十毫秒前某些已經發出去的 ID 撞在同一個毫秒。偏偏那個瞬間又是流量小、產生的 ID 少的深夜,序號都從很小的值開始,於是「同一個時間戳記 + 同一台機器 + 同樣從 0 開始的序號」——ID 就這麼撞了。

當下怎麼止血

凌晨的第一要務是止血,不是找根因。我做了兩件事:

  • 先把那台出問題的機器從 ID 產生的節點池裡踢掉,讓流量走其他時鐘正常的節點,撞號立刻停了。
  • 然後撈出那段時間所有重複的 ID,跟業務資料比對,人工把受影響的少數訂單挑出來單獨處理。還好深夜量小,受影響的是個位數。

止完血我才敢回去睡,根因是隔天上班才好好查的。

真正的修法:面對時鐘會倒退這件事

事後我把 ID 產生器改了,核心思想是:不要再天真地相信時鐘只會往前走,要主動偵測倒退並處理。

具體來說,產生器會記住「上一次發 ID 用的時間戳記」。每次要發新 ID 時,拿現在的時間跟它比:

  • 如果現在時間大於上次,正常,更新記錄、序號歸零。
  • 如果現在時間等於上次,同一毫秒,序號遞增;序號用完就自旋等到下一毫秒。
  • 如果現在時間小於上次——時鐘倒退了。這時候絕對不能發 ID

最後這個 case 怎麼處理,是重點。我的選擇是:如果倒退的幅度很小(例如幾毫秒內),就讓產生器稍微等一下,等實體時間追上「上次記錄的時間戳記」再繼續發,寧可卡幾毫秒也不發錯。如果倒退幅度大到不合理,那就直接拒絕發 ID、丟出明確的錯誤並告警,讓這台節點停止服務,也不要冒著撞號的風險硬產。

寧可短暫不可用,也不要產生錯誤的資料。 這是我從那晚學到、後來套用在很多地方的原則。

幾個延伸的取捨

改完之後我還想通了幾件事:

  • 機器 ID 的分配也要小心。如果兩台機器不小心拿到同一個機器 ID,那連時鐘正常都會撞號。我們後來把機器 ID 的分配交給一個集中的協調服務發放,而不是靠設定檔手動填,就是怕人為填重複。
  • NTP 的校時策略可以調。可以把 NTP 設成用 slew(緩慢調整、只放慢或加快時鐘、絕不往回跳)而不是 step(直接跳)。這在系統層面就減少了時鐘倒退的機會。不過我不會只靠這個——應用層自己還是要能扛,因為你不能假設每台機器的 NTP 都設對了。
  • 要不要換一種不依賴時間的 ID? 我評估過 UUID 這類方案,它不依賴時鐘,但缺點是無序、當主鍵對資料庫索引不友善。權衡下來,我們的場景還是需要大致遞增的 ID,所以選擇留在 Snowflake,但把時鐘倒退處理好。沒有銀彈,只有適合你場景的取捨。

小結

這件事最讓我印象深刻的,不是演算法多難,而是它打穿了我一個沒意識到的假設——「時間會單調遞增」。我們寫程式時埋了太多這種「想當然爾」的前提,平常都好好的,直到某個凌晨,某個你從沒懷疑過的基礎設施抖了一下,才把它們一次暴露出來。所以現在只要看到程式碼把時間當成某種保證來用,我都會多問一句:那如果時鐘倒退了呢?

#分散式ID#Snowflake#時鐘同步#高併發#系統設計#事故覆盤

留言討論

有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。

相關文章