每台機器都有自己的石英鐘,各自漂移、還可能被 NTP 悄悄回撥。於是「現在幾點」在分散式系統裡不是一個事實,而是一個帶誤差的估計。把這個估計當成精確時間去排序事件,換來的往往不是崩潰,而是更可怕的東西——靜默的資料遺失。
🧠 Intuition
TIP
先分清兩種時鐘,混用是萬惡之源:日曆時鐘 (time-of-day) 回答「現在是幾月幾號幾點」——但它會跳、會被 NTP 回撥;單調時鐘 (monotonic) 只保證「只增不減」——適合量「過了多久」,但它的絕對值毫無意義、跨機器不能比。量間隔用單調鐘,別用日曆鐘。
- 日曆時鐘(
CLOCK_REALTIME/System.currentTimeMillis()):與 NTP 同步、跨機器語意相同;但可能被強制回撥、常忽略閏秒,不適合量經過時間。 - 單調時鐘(
CLOCK_MONOTONIC/System.nanoTime()):保證只向前、絕不跳;絕對值無意義、跨機器無法比較;NTP 只能調它前進的速率 (slewing)、不會讓它跳。 - 石英會漂移:Google 假設漂移率 200 ppm,一天不同步就偏 17 秒;NTP 也受網路延遲限制(網際網路上最低誤差約 35 ms)。
⚖️ Tradeoffs
NTP 同步遠不如你以為的可靠
- 本地偏差過大時 NTP 會強制重設、造成時間跳躍;防火牆可能悄悄擋掉 NTP 流量;有些 NTP 伺服器本身就報錯時間。
- 閏秒讓一分鐘變 59 或 61 秒,曾弄垮許多大型系統;VM 時鐘在 CPU 共享時看似瞬間跳前;使用者的裝置更可能故意設錯。
- 最危險的不是崩潰,而是靜默偏離:大部分功能看起來正常,時鐘卻悄悄漂離現實——所以必須主動監控機器間的時鐘偏移。
用時間戳排序事件的陷阱:LWW 丟資料
- 多主複製想用日曆時鐘的時間戳來排序寫入,很誘人卻危險。
- 經典反例:A 在 node 1 寫
x=1(戳記 42.004),B 隨後在 node 3 把它x=2(戳記 42.003,因為 node 3 時鐘慢)。複製到 node 2 時,它看戳記大小、誤以為x=1較新而丟掉x=2——B 的操作靜默消失。 - 這正是 Last Write Wins (LWW) 的病(Cassandra、Riak 廣用):時鐘落後的節點寫的值永遠贏不過時鐘超前的;更根本地,它無法區分真正並行與有因果先後的寫入。
- 更安全的替代是邏輯時鐘 (logical clock):用遞增計數器追蹤事件相對順序,而非量實際時間。
把時鐘讀數當成區間:TrueTime 與 commit-wait
- 時鐘讀數不該被當成一個時間點,而是一個信賴區間 (confidence interval):例如「有 95% 把握現在落在 10.3–10.5 秒之間」。若不確定度是 ±100 ms,時間戳寫到微秒位純屬自欺。
- Google TrueTime(用於 Spanner)是少數明確回報區間
[earliest, latest]的 API。 - Spanner 用它做跨資料中心的一致性:兩交易區間不重疊就能定先後;重疊就刻意等一個區間長度再提交(commit-wait),確保因果順序。為縮短這個等待,Google 在每個機房裝 GPS 接收器或原子鐘,把不確定度壓到約 7 ms。
🔑 Takeaways
- 量間隔用單調鐘、標時刻用日曆鐘——混用是分散式時間 bug 的頭號來源。
- NTP 給的是估計不是真相:回撥、閏秒、防火牆、VM 跳躍都會讓它出錯,而且錯得安靜——要監控時鐘偏移。
- 拿日曆時間戳排序事件會踩 LWW 靜默丟資料的坑;要排序因果,用邏輯時鐘。
- 若真要用物理時間做正確性,就別假裝它精確——把它當信賴區間,像 Spanner 那樣用 commit-wait 把不確定度變成可以等待的成本。
No notes yet — jot your takeaways or Q&A here.