一致性雜湊:讓節點上下線只搬必要的資料
Consistent Hashing 把節點與資料鍵放到同一個 hash ring 上,讓擴容、縮容、節點故障時只重新分配相鄰區間,而不是讓整個叢集大洗牌。它是 cache cluster、distributed storage、load balancer 與 sharded database 中最常見的資料分派基礎。
總覽與定位
為什麼普通 modulo sharding 在擴縮容時會失控,而 hash ring 可以把搬遷限制在局部。
最直覺的 sharding 做法是把資料鍵做 hash,再用節點數取餘數:hash(key) % N。問題在於 N 一變,幾乎每個 key 的目標節點都會改變。對 cache 來說這會造成大量 miss;對 storage 來說這代表昂貴的 data migration;對 online service 來說,擴容本來是為了降載,卻可能先製造一波重分片風暴。
資料不再依賴 % N,而是被映射到固定大小的環上。key 往順時針找到的第一個節點就是 owner;新增或移除節點時,只會影響該節點前一段區間。
| 策略 | 路由方式 | 節點數變動時 | 典型用途 |
|---|---|---|---|
| Modulo Hashing | hash(key) % N |
大多數 key 重新指派 | 固定節點數、離線批次分桶 |
| Consistent Hashing | hash ring 順時針找 successor | 只搬受影響的相鄰區間 | Cache、storage node、動態 shard pool |
| Rendezvous Hashing | key 對所有節點打分取最高 | 只影響加入或移除節點相關 key | 節點數中小、需要簡潔實作的 client-side routing |
一致性雜湊不是讓資料永不移動,而是讓移動範圍與變動節點相關,而非與整個叢集規模相關。
% N,因此節點上下線時只重分配局部區間,降低 cache miss 與資料搬遷成本。
Ring 演算法
把節點與 key 映射到同一個環,使用 successor rule 決定 owner。
一致性雜湊通常把 hash space 視為一個首尾相連的範圍,例如 0..359 或 0..2^32-1。每台節點先用 node id 做 hash 放上 ring;每個資料鍵也做 hash 放上 ring。查詢時從 key 的位置順時針走,遇到的第一個節點就是負責該 key 的 owner。
當新增節點 D 時,它只會接手自己與前一個節點之間的 key;當節點 B 掛掉時,原本屬於 B 的 key 會順時針交給下一個節點。其他 ring 區間不需要搬動。
若只有少量實體節點,每台機器在 ring 上的位置可能剛好很近或很遠,導致負載不均。Production 實作幾乎都會搭配 virtual nodes 或 weighted replicas。
function lookup(key, sortedRing) {
const point = hash(key);
const owner = sortedRing.find(node => node.point >= point);
return owner || sortedRing[0]; // wrap around
}
Virtual Nodes
用多個虛擬節點攤平負載,也讓不同容量的機器可以用權重參與分配。
Virtual Node(vnode)是把一台實體節點映射成多個 ring 上的位置,例如 A#1、A#2、A#3。key lookup 仍然找到第一個 vnode,但實際請求會送到該 vnode 背後的實體節點。vnode 越多,隨機 hash 造成的不均就越容易被平均掉。
| 設計點 | 沒有 vnode | 有 vnode |
|---|---|---|
| 負載平衡 | 少量節點時容易偏斜 | 每台節點散布多段區間,分配更平滑 |
| 異質容量 | 所有節點大致同權 | 大機器給更多 vnode,小機器給較少 vnode |
| 搬遷粒度 | 一次搬一大段連續區間 | 拆成多個小區間,可逐步搬遷與節流 |
| 維運成本 | metadata 少 | ring metadata 較多,需要一致發布 |
vnode 數不是越多越好。太少會負載不均;太多會增加 routing table、membership gossip、rebalance plan 的成本。常見做法是按服務規模選一個足以平滑分布的 vnode 數,再用容量權重微調。
擴縮容與故障
一致性雜湊降低 remapping,但 production 還要處理資料搬遷、複本與 membership 一致性。
在真實系統裡,ring 變更不是單純把節點加入陣列。資料或 cache entry 需要搬遷,client 需要拿到同一版 ring,replica placement 不能把多個複本放在同一故障域,否則一次 rack failure 仍然可能打掉多份資料。
- Membership change:控制面確認節點健康、容量與版本,再產生新的 ring version。
- Data movement:新 owner 從舊 owner backfill key range;過程中可能需要 dual read 或 forwarding。
- Traffic cutover:client 或 gateway 切到新 ring version,必要時保留短暫相容窗口。
- Replica awareness:第二、第三複本通常取後續不同 failure domain 的節點,而不是只看下一個 vnode。
它讓「哪些 key 需要搬」變少,但仍需要流量節流、背景複製、健康檢查、ring version 管理與監控告警,才能安全地在 production 擴縮容。
互動 Demo
切換情境,觀察新增或移除節點時哪些 key 真的需要 remap。
下方模擬器用簡化的 0..359 ring 展示 4 個實體節點與 12 個 key。切換情境後,受影響的 key 會被標記出來;你可以直觀看到一致性雜湊的核心價值:不是零搬遷,而是只搬必要的一小段。
| Key | Hash point | 原 owner | 新 owner | 狀態 |
|---|
面試取捨與設計檢查表
回答題目時要把 ring、vnode、replica 與 operational safety 一起講清楚。
面試中提到一致性雜湊時,不要只畫一個環。更完整的回答應該說明它解決了什麼、沒有解決什麼,以及你如何把它放進可運維的系統裡。
| 面向 | 應該說清楚 | 常見追問 |
|---|---|---|
| Correctness | 所有 client 需要使用同一版 ring,否則同一 key 可能被送到不同 owner。 | ring version 如何發布?舊版本請求如何相容? |
| Load Balance | 少量實體節點會偏斜,需使用 vnode 與容量權重。 | 如何避免 hot key 打爆單一 node? |
| Availability | 故障節點的 range 要轉交 successor,但資料系統還需要 replica。 | 副本如何避開同 rack / zone? |
| Operations | 擴縮容要節流搬遷,避免 backfill 與 foreground traffic 互相干擾。 | 如何監控 remap rate、rebalance lag、cache miss? |
若某個 key 本身流量巨大,不管它被分到哪個節點都可能形成 hotspot。此時需要 request coalescing、local cache、key splitting、replication read fanout 或應用層降載。