分散式系統的時間與順序
每台機器的時鐘都不準:用時間戳排序,回覆可能排到原訊息前面。Lamport 時鐘和向量時鐘不靠實際時間,只記錄「誰發生在誰之前」。
20 ms
排序依據
點一個事件,看其他事件和它的關係。
選中的事件發生在它之前發生在它之後並行:分不出先後
依「各自的時鐘」排序:
- B1 -11
- A1 8
- B2 9
- B3 17 ✕
- C1 17
- A2 20
- B4 22 ✕
- B5 27 ✕
- C2 30
- A3 31
- C3 33
- A4 39
- B6 42 ✕
- A5 48
- B7 54 ✕
- C4 54
- C5 54
- C6 58
- A6 85
時鐘排序違反因果
8
Lamport 排序違反因果
0
收到時間早於送出
2 / 10
總事件數
19
B3 收到 C1 送來的訊息:Lamport 值取 max(自己, 1) + 1 = 3;向量每一格取兩邊較大的,再把自己那格加 1:[0,3,1]。 比較向量:3 個事件發生在它之前、5 個在之後,10 個是並行的——兩者之間沒有任何訊息鏈相連,哪個時鐘都不該替它們排先後。 用各自的時鐘排序,有 8 對違反因果。
模型:每個時鐘偏差固定(A +0.3 倍、B −1 倍、C +0.7 倍的誤差設定),走速正確;訊息延遲 4–40 ms,後送的可能先到。「發生在之前」直接從訊息關係算出,不看任何時鐘。排序時同值的依行程名稱排。
亮起來的是這一步執行的程式碼
class LamportClock { time = 0; tick(): number { // a local event or a send this.time += 1; return this.time; } receive(stamp: number): number { this.time = Math.max(this.time, stamp) + 1; return this.time; }} class VectorClock { v: number[]; constructor(private me: number, processes: number) { this.v = new Array(processes).fill(0); } tick(): number[] { // a local event or a send this.v[this.me] += 1; return [...this.v]; // a send carries this copy } receive(stamp: number[]): number[] { for (let i = 0; i < this.v.length; i++) { this.v[i] = Math.max(this.v[i], stamp[i]); } this.v[this.me] += 1; return [...this.v]; }} // "before": a happened before b. "concurrent": neither saw the other.function compare(a: number[], b: number[]): string { let less = false, more = false; for (let i = 0; i < a.length; i++) { if (a[i] < b[i]) less = true; if (a[i] > b[i]) more = true; } if (less && !more) return "before"; if (more && !less) return "after"; return less ? "concurrent" : "equal";}模型假設與範圍
- 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
- 三個程序的固定訊息圖提供因果基準;Lamport 同序值不表示因果,向量時鐘也不表示真實經過秒數。
什麼時候用
- 只要一個和因果一致的全序:用 Lamport 時鐘(再用節點編號打破平手)。例如替所有操作排一個大家都同意、又不會讓「回覆」排在「原訊息」前面的順序。
- 要知道兩件事是不是並行(衝突):用向量時鐘或版本向量。兩個副本各自改了同一筆資料,只有向量能看出「誰也沒看過誰的修改」,該合併而不是蓋掉。
- 要和真實世界的時間對得上(「下午三點前下單」),才用實體時鐘,而且要知道誤差範圍。
和其他主題的關係
- 被這些用到
- 案例:分散式鍵值儲存案例:協同編輯
出現在這些架構裡
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| Lamport:本地事件或收訊 | O(1) | O(1) |
| 向量時鐘:收訊合併 P 是行程數:每一格取最大值 | O(P) | O(P) |
| 向量時鐘:比較兩個事件 | O(P) | O(P) |
空間:O(P),向量時鐘每則訊息都要帶 P 個數字;Lamport 只帶 1 個
和其他做法比
| 時鐘排序違反因果 | 收到時間早於送出 | Lamport 排序違反因果 | 並行的配對 | |
|---|---|---|---|---|
| 誤差 0 ms | 0 | 0 | 0 | 2592 / 6774 |
| 誤差 5 ms | 15 | 11 | 0 | 2592 / 6774 |
| 誤差 20 ms | 235 | 93 | 0 | 2592 / 6774 |
| 誤差 50 ms | 694 | 152 | 0 | 2592 / 6774 |
每列 40 組隨機訊息往來(同樣的種子,只改時鐘誤差)。並行的配對和時鐘無關:它是訊息圖本身的性質,所以每列都一樣——超過三分之一的事件對,根本沒有先後可言。
真實世界裡的它
- Dynamo、Riak 用版本向量偵測同一個鍵的衝突寫入,把兩個版本都交給應用程式合併。
- Google Spanner 的 TrueTime 給出時間的誤差區間,提交時等過誤差才回覆;CockroachDB 用混合邏輯時鐘 HLC(Hybrid Logical Clock)。
- 聊天室的訊息順序:伺服器替每個對話發遞增序號,而不是相信手機的時鐘(見即時通訊)。
取捨與陷阱
- 用時間戳排序會讓回覆跑到問題前面:時鐘差幾毫秒就夠了(上表誤差 5 ms 時,已有 11 則訊息「收到」早於「送出」)。最後寫入勝出也因此會丟掉其實比較新的寫入。
- Lamport 值小不代表先發生:L(a) < L(b) 不能推出 a 在 b 之前,只有反過來成立。要判斷並行,必須用向量時鐘。
- 向量時鐘隨參與者數量變大:成千上萬個用戶端各佔一格就太大了。實務上只讓伺服器(副本)佔格,或定期修剪。
- NTP(Network Time Protocol)校時會讓時鐘跳動甚至倒退;量時間間隔要用單調時鐘(monotonic clock),不要用牆上時間相減。