一致性雜湊
把資料分散到好幾台機器上,而且加減一台機器時只需要搬一小部分資料,不是全部重新洗牌。
10
每台分到的鍵(共 4,000 個)
- A1033
- B1454
- C1513
虛線:完全平均時每台該分到的量。
最忙/平均
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 O | n = 10 | n = 100 | n = 1,000 | n = 10,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 雜湊環:二分搜尋 | O(log n) | 3.5 | 6.8 | 10.0 | 13.4 | ×3.8 (×4.0) |
| hash mod N | O(1) | 1 | 1 | 1 | 1 | ×1.0 (×1.0) |
環上的點變成一千倍,查一個鍵只多了十步左右。mod N 永遠一步,但它的代價在加減機器時才付:幾乎所有鍵都要搬。
和其他做法比
| 4 台加到 5 台時搬動的鍵 | 最忙/平均(4 台) | 查一個鍵的成本 | |
|---|---|---|---|
| hash mod N | 80% | 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)雜湊後面多加了一道混合,否則同一台的虛擬節點會擠在一起。