跳到主要內容

系統設計

把資料結構放大到好幾台機器

主題 · 分散式系統的時間與順序

分散式系統的時間與順序

每台機器的時鐘都不準:用時間戳排序,回覆可能排到原訊息前面。Lamport 時鐘和向量時鐘不靠實際時間,只記錄「誰發生在誰之前」。

20 ms
排序依據

點一個事件,看其他事件和它的關係。

行程 A時鐘 +6 ms行程 B時鐘 −20 ms行程 C時鐘 +14 msA1 · 時鐘 8 ms · Lamport 1 · [1,0,0]A1C1 · 時鐘 17 ms · Lamport 1 · [0,0,1]C1B1 · 時鐘 -11 ms · Lamport 1 · [0,1,0]B1A2 · 時鐘 20 ms · Lamport 2 · [2,1,0]A2C2 · 時鐘 30 ms · Lamport 2 · [0,0,2]C2C3 · 時鐘 33 ms · Lamport 3 · [0,1,3]C3A3 · 時鐘 31 ms · Lamport 3 · [3,1,0]A3B2 · 時鐘 9 ms · Lamport 2 · [0,2,0]B2A4 · 時鐘 39 ms · Lamport 4 · [4,2,0]A4B3 · 時鐘 17 ms · Lamport 3 · [0,3,1]B3C4 · 時鐘 54 ms · Lamport 4 · [0,1,4]C4C5 · 時鐘 54 ms · Lamport 5 · [1,1,5]C5A5 · 時鐘 48 ms · Lamport 5 · [5,2,0]A5B4 · 時鐘 22 ms · Lamport 4 · [3,4,1]B4C6 · 時鐘 58 ms · Lamport 6 · [3,1,6]C6B5 · 時鐘 27 ms · Lamport 5 · [3,5,4]B5B6 · 時鐘 42 ms · Lamport 6 · [3,6,4]B6B7 · 時鐘 54 ms · Lamport 7 · [5,7,4]B7A6 · 時鐘 85 ms · Lamport 7 · [6,6,4]A6真實時間 →
選中的事件發生在它之前發生在它之後並行:分不出先後
依「各自的時鐘」排序:
  1. B1 -11
  2. A1 8
  3. B2 9
  4. B3 17 ✕
  5. C1 17
  6. A2 20
  7. B4 22 ✕
  8. B5 27 ✕
  9. C2 30
  10. A3 31
  11. C3 33
  12. A4 39
  13. B6 42 ✕
  14. A5 48
  15. B7 54 ✕
  16. C4 54
  17. C5 54
  18. C6 58
  19. 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 ms0002592 / 6774
誤差 5 ms151102592 / 6774
誤差 20 ms2359302592 / 6774
誤差 50 ms69415202592 / 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),不要用牆上時間相減。