跳到主要內容

系統設計

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

主題 · 一致性雜湊

一致性雜湊

把資料分散到好幾台機器上,而且加減一台機器時只需要搬一小部分資料,不是全部重新洗牌。

10
順時針 →30 個節點
每台分到的鍵(共 4,000 個)
  • A
    1033
  • B
    1454
  • C
    1513

虛線:完全平均時每台該分到的量。

最忙/平均
1.13×
上次變動:環上搬動
—
同樣變動:mod N 搬動
—

每台伺服器在環上出現 10 次;每個鍵歸順時針方向遇到的第一台伺服器管。加入或移除一台伺服器,看看有多少鍵得搬家。

亮起來的是這一步執行的程式碼
function hash(key: string): number {
let h = 0x811c9dc5;
for (const byte of new TextEncoder().encode(key)) {
h = Math.imul(h ^ byte, 0x01000193);
}
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
return (h ^ (h >>> 16)) >>> 0;
}
class HashRing {
private points: number[] = []; // sorted positions
private owner = new Map<number, string>();
constructor(private vnodes: number) {}
addServer(name: string): void {
for (let r = 0; r < this.vnodes; r++) {
const p = hash(`${name}#${r}`);
this.points.splice(this.firstAtOrAfter(p), 0, p);
this.owner.set(p, name);
}
}
removeServer(name: string): void {
this.points = this.points.filter((p) => this.owner.get(p) !== name);
}
getServer(key: string): string {
const p = hash(key);
let i = this.firstAtOrAfter(p);
if (i === this.points.length) i = 0;
return this.owner.get(this.points[i])!;
}
private firstAtOrAfter(p: number): number {
let lo = 0, hi = this.points.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.points[mid] < p) lo = mid + 1;
else hi = mid;
}
return lo;
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 鍵與虛擬節點依固定雜湊放上環;衡量擁有者改變與鍵數分布。未搬移資料,也未估算網路與副本重建時間。

什麼時候用

  • 把資料或快取分散到好幾台機器,而且機器會增減(擴容、故障、維修)時。
  • 希望同一個鍵總是送到同一台機器(同一個用戶的 session、同一個物件的快取),又不想用一張中央對照表時。

和其他主題的關係

延伸閱讀
快取負載平衡

出現在這些架構裡

時間與空間複雜度(Big O)

操作平均最差
查一個鍵在哪台
M = N·V,環上的點數;mod N 是 O(1)
O(log M)O(log M)
加入一台
TypeScript 和 Python 把 V 個點插進排序好的陣列,每次要搬;Java 的 TreeMap 是 O(V log M)
O(V·M)O(V·M)
移除一台O(M)O(M)
加入一台時要搬的鍵
K 是鍵的總數;hash mod N 幾乎是全部 K 個
≈ K/(N+1)K

空間:O(N·V),每個虛擬節點一個位置

Big O 實測:n 變大時步數怎麼長

數的是:查一個鍵要做幾步(n = 環上的點數,1,000 個鍵取平均)

Big On = 10n = 100n = 1,000n = 10,000成長倍數:實測(理論)
雜湊環:二分搜尋O(log n)3.56.810.013.4×3.8 (×4.0)
hash mod NO(1)1111×1.0 (×1.0)

環上的點變成一千倍,查一個鍵只多了十步左右。mod N 永遠一步,但它的代價在加減機器時才付:幾乎所有鍵都要搬。

和其他做法比

4 台加到 5 台時搬動的鍵最忙/平均(4 台)查一個鍵的成本
hash mod N80%1.02×一次取餘數
雜湊環,每台 1 個虛擬節點8%2.19×在 4 個節點裡二分搜尋
雜湊環,每台 10 個虛擬節點15%1.13×在 40 個節點裡二分搜尋
雜湊環,每台 100 個虛擬節點25%1.10×在 400 個節點裡二分搜尋

10,000 個鍵。理想情況下加入第 5 台只需要搬 1/5 = 20% 的鍵;mod N 則幾乎全部搬家。虛擬節點越多,每台分到的量越平均,但環變大、查找多幾步二分搜尋。

真實世界裡的它

  • Amazon Dynamo、Cassandra、Riak 用雜湊環決定每筆資料放在哪幾台。
  • Memcached 的用戶端函式庫(ketama)用一致性雜湊分配快取伺服器。
  • 負載平衡器依用戶或連線做雜湊(例如 Envoy 的 ring hash、Maglev),讓同一個用戶落在同一台。

取捨與陷阱

  • 每台只放一個節點時,分到的弧長差很多,負載會很不平均;虛擬節點就是為了這個。
  • 搬得少不等於不用搬:新機器加入時,它接手的那一份資料還是得從別台複製過來,期間要小心讀不到。
  • 雜湊函數要把相似的名字(A#1、A#2…)打散;這裡在 FNV-1a(Fowler–Noll–Vo)雜湊後面多加了一道混合,否則同一台的虛擬節點會擠在一起。