分散式時間#

在單機上「現在幾點」「誰先誰後」是顯而易見的;在分散式系統裡,這兩個問題都會出錯。每台機器有自己的時鐘,彼此會漂移、會被校正回跳,網路延遲又讓「同時」失去意義。本章說明為什麼不能信任實體時鐘來排序事件,以及邏輯時鐘(Lamport、向量時鐘)如何在不依賴實體時間的前提下,捕捉事件的因果關係。

這裡建立的因果判斷工具,直接支撐 複製模型 的衝突解決與 一致性模型 的因果一致性。

兩種時鐘:日曆 vs 單調#

每台機器其實有兩種不同用途的時鐘,混用是常見 bug 來源:

時鐘回傳特性適用
日曆時鐘 (Wall-clock)真實世界時間(如 Unix 時間)會被 NTP 校正,可能往回跳顯示時間、記錄時間戳
單調時鐘 (Monotonic clock)任意起點的遞增計數只會前進、不回跳,但跨機器不可比較量測「經過了多久」、逾時計算

用日曆時鐘量測「經過了多久」是經典錯誤:NTP 校正可能讓日曆時鐘往回跳,算出負的耗時,或讓逾時邏輯瞬間失效。量測時間間隔一律用單調時鐘。

NTP、時鐘漂移與無聲的資料丟失#

機器靠 NTP (Network Time Protocol) 對時,但 NTP 本身受網路延遲影響,精度通常只有數毫秒到數十毫秒,極端情況更差;石英振盪器還會持續漂移 (clock drift)。這意味著兩台機器的日曆時鐘隨時可能相差數十毫秒。

把這種不可靠的時鐘拿來排序寫入,會造成無聲的資料丟失

LWW(最後寫入勝出)配上偏差的時鐘:

主A 時鐘快了 100ms
  T_real=0ms   客戶端在主A 寫 x=1,時間戳被打成 100
  T_real=20ms  客戶端在主B 寫 x=2,時間戳是 20(較晚卻較小)

LWW 比時間戳:100 > 20 → 保留 x=1
結果:較晚、客戶端以為成功的 x=2 被默默丟棄,且沒有任何錯誤

「無聲」之所以致命,是因為系統不會報錯,資料就這麼不見了。這就是 複製模型 中反覆警告「LWW 危險」的根因。要正確排序事件,需要的不是更準的時鐘,而是邏輯時鐘

Lamport 時間戳:全序但滯後#

Lamport 時間戳用一個遞增計數器取代實體時間,捕捉「可能的因果」。規則極簡:

每個節點維護 counter

本地事件 / 送出訊息:
    counter = counter + 1
    (送訊息時附帶 counter)

收到訊息(帶 ts):
    counter = max(counter, ts) + 1

比較時以 (counter, nodeId) 為鍵,counter 相同則用 nodeId 打破平手——於是任意兩事件都能排出全序 (total order)

它保證:若 A 因果先於 B(A ➡️ B),則 LC(A) < LC(B)

反向不成立LC(A) < LC(B) 不代表 A 因果先於 B,它們可能只是並行。Lamport 時間戳給的是一個「不違反因果」的全序,卻無法分辨因果與並行——這就是「滯後」之處。要區分並行,需要向量時鐘。

向量時鐘:判斷因果與並行#

向量時鐘 (Vector Clock) 讓每個節點維護一個向量,記錄它所知道的「每個節點各自走到第幾步」。

n 個節點,每個維護長度 n 的向量 V,初始全 0

本地事件 / 送出訊息(節點 i):
    V[i] = V[i] + 1
    (送訊息時附帶整個 V)

收到訊息(帶向量 W,節點 i):
    for k in 0..n-1:
        V[k] = max(V[k], W[k])
    V[i] = V[i] + 1

比較 Va 與 Vb:
    若 對所有 k 有 Va[k] <= Vb[k] 且至少一處 < :  A → B(A 因果先於 B)
    若 對所有 k 有 Va[k] >= Vb[k] 且至少一處 > :  B → A
    否則(互有大小):                            A ∥ B(並行,這就是衝突)
sequenceDiagram
    participant A as 節點A
    participant B as 節點B
    Note over A: e1: [1,0]
    A->>B: 送訊息 [1,0]
    Note over B: e2: [1,1] (因果 e1→e2)
    Note over B: e3: [1,2]
    Note over A: e4: [2,0] (與 e2,e3 並行 ∥)

如上:e2 = [1,1] 因果依賴 e1 = [1,0];而 e4 = [2,0]e2 = [1,1] 互有大小(A 維度 2>1,B 維度 0<1),判定為並行——正是需要交給應用仲裁的真正衝突。

三種時鐘對比#

機制能否全序能否分辨因果/並行開銷主要用途
實體時鐘 + LWW是(靠時間戳)否(且可能排錯)極低可容忍丟資料的場景
Lamport 時間戳否(只保證不違反因果)低(單一計數器)需要一個一致全序
向量時鐘否(偏序)高(與節點數成正比)衝突偵測、因果一致性

選擇原則:只需要「一個大家同意的順序」用 Lamport;需要「判斷兩個寫入是否衝突」用向量時鐘;只有在資料可丟的場景才用實體時鐘 LWW。

Google TrueTime:把不確定性顯式化#

多數系統假裝時鐘是準的,TrueTime 反其道而行——它承認時鐘有誤差,並把誤差量化成一個區間。呼叫 now() 不回傳一個時間點,而是回傳區間 [earliest, latest],保證真實時間落在其中。

TT.now() = [earliest, latest],區間寬度 = 時鐘不確定性 ε(毫秒級)

要保證事務 T2 的時間戳一定晚於 T1:
    commit T1,取得時間戳 s
    「commit-wait」:等到 TT.now().earliest > s 才讓 T1 對外可見
    → 確保任何後來者的區間都晚於 s

代價是每次提交要等待一個 ε 寬度的時間(commit-wait),用「主動等待時鐘不確定性過去」換取跨地理區的外部一致性。這需要 GPS 與原子鐘把 ε 壓到很小,是少數能在全球規模提供強一致時間戳的方案。

TrueTime 的洞見不在硬體,而在觀念:與其假裝時鐘準確,不如把不確定性顯式化並等它過去。大多數系統做不到原子鐘,所以更該回頭依賴邏輯時鐘來排序,而非實體時間。

小結#

  • 分清日曆時鐘(會回跳,只能顯示)與單調時鐘(量間隔用)。
  • NTP 有毫秒級誤差且時鐘會漂移,拿實體時間排序寫入會造成 LWW 無聲資料丟失
  • Lamport 時間戳給全序但分不出因果與並行;向量時鐘能精確判斷 ,代價是開銷隨節點數成長。
  • TrueTime 把時鐘不確定性量化成區間並主動等待,是用硬體與等待換全球強一致的特例。