分散式時間#
在單機上「現在幾點」「誰先誰後」是顯而易見的;在分散式系統裡,這兩個問題都會出錯。每台機器有自己的時鐘,彼此會漂移、會被校正回跳,網路延遲又讓「同時」失去意義。本章說明為什麼不能信任實體時鐘來排序事件,以及邏輯時鐘(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 把時鐘不確定性量化成區間並主動等待,是用硬體與等待換全球強一致的特例。