19.6 有界负载一致性哈希
一句话
普通一致性哈希只保证 “增删节点时迁移少”,不保证 “负载均”:某些节点可能拿到 2 倍平均量;有界负载版本给每个节点设上限
(1+ε)×平均,超限就沿环找下一个未满节点——用少量额外查询换可证明的负载上限。
为什么需要 “有界”
| 问题 | 普通一致性哈希 | 有界版本 |
|---|---|---|
| 负载方差 | 随节点数增加而下降,但永不为零(O(1/√n) 量级) | 硬上限 (1+ε)×平均 |
| 热点 key | 落在哪个节点就压垮哪个 | 该节点满后可外溢到邻居 |
| 迁移量 | 只动相邻区间 | 可能多动 “外溢” 的 key |
| 实现复杂度 | 低(环 + 二分) | 中(需维护每节点负载计数) |
虚拟节点(见 19.5 虚拟节点)只是 “把环切得更碎”,降低方差但不设上限;面对 Zipf 分布的长尾请求,仍可能出现单节点超载。
算法:环 + 容量检查
1. 把节点按 hash 放到环上(可带虚拟节点)
2. key → hash 落在环上某位置,顺时针找到第一个节点 n
3. 若 n 的当前负载 < (1+ε) × 平均负载 → 分配给 n
4. 否则继续顺时针找下一个未满节点(可限制最多探测 k 个)
5. 都满则拒绝/排队(不能无界外溢)
关键的工程取舍是 ε 与探测上限 k:ε 越小越均衡,但外溢越多、查询越慢;k 限制最坏情况的查找成本。
数字:ε 的影响
| ε | 最大负载(相对平均) | 典型外溢比例 | 说明 |
|---|---|---|---|
| 0.05 | 1.05× | 较高(长尾 key 频繁外溢) | 均衡最好,查找最贵 |
| 0.25 | 1.25× | 中 | 常用折中 |
| 0.5 | 1.5× | 低 | 简单,但热点节点仍可能过载 |
(上表是量级示意,具体取决于 key 分布与节点数。)
实测:虚拟节点 ≠ 负载均衡
本机 Python 3.14 实测(100 节点、每节点 200 虚节点、20 万 key,blake2b 哈希):
| 分布 | 策略 | 最大/均值 | P99/均值 | 外溢比例 |
|---|---|---|---|---|
| 均匀 | 一致性哈希(200 虚节点) | 1.20 | 1.20 | 0 |
| Zipf α=1.1 | 一致性哈希(200 虚节点) | 7.22 | 7.22 | 0 |
| Zipf α=1.1 | 有界负载 ε=0.25 | 1.25 | 1.25 | 21.9% |
两个结论:① 均匀分布下虚节点已经够好(1.20);② 热点分布下普通一致性哈希的最大负载是均值的 7.2 倍——这就是 “扩容也救不了热点” 的数字版本;③ 有界负载把上限压到 1.25,代价是 21.9% 的 key 需要外溢(也就是每次路由可能多探测几跳)。
# 有界分配的核心:满了就顺延到环上下一个未满节点
cap = math.ceil(1.25 * total_keys / nodes) # ε = 0.25
for k in keys:
idx = ring.locate(h(k))
for probe in range(len(ring.points)):
owner = ring.owner[(idx + probe) % len(ring.points)]
if load[owner] < cap: # 有界:满了不许再收
load[owner] += 1; break
反模式
| 反模式 | 后果 |
|---|---|
| 只加虚拟节点就宣称 “负载均衡” | 长尾下仍超载,且无法解释 |
| 外溢无上限,绕环找一圈 | 热点时一次请求 O(n) 探测 |
| 负载计数用本地内存 | 多协调者时计数不一致,上限失效 |
| 忽略 “节点容量不同” | 同权重分配,大机器闲置、小机器过载 |
三条铁律
🔑 一致性哈希保迁移,不保均衡;均衡要靠 “容量上限” 或 “加权”。
🔑 ε 是可调的 SLO:越小越均衡,代价是查找与外溢。
🔑 外溢必须有界;否则热点会把 “查找” 变成 “广播”。
自检
Q1 为什么普通一致性哈希在 Zipf 分布下仍会出现单节点过载?
▸ 答案:因为哈希只保证 “key 均匀落到环上”,而访问频率并不均匀:少数热点 key 携带大部分请求,它们落在哪个节点,哪个节点就被压垮。虚拟节点只让 “区间更碎”,无法改变 “热点 key 只属于一个节点” 的事实。
Q2 有界负载如何在不破坏一致性的前提下外溢?
▸ 答案:外溢只影响 “路由决策”,不影响 “数据归属的确定性”:给定同一套节点与负载状态,路由结果可复现;节点变化时按环重算。代价是外溢的 key 在节点恢复后要迁回(会产生额外迁移),所以外溢比例要控制在较低水平。
Q3 ε 设得很小(如 0.01)会有什么问题?
▸ 答案:外溢比例升高、查找路径变长:大量 key 需要跳过已满节点,最坏情况下探测多个节点,尾延迟上升;同时 key 更容易因负载波动 “来回搬迁”。实用区间通常在 0.1–0.3。
Q4 多协调者场景下,负载计数为什么会不一致?
▸ 答案:因为每个协调者只知道 “自己分配出去的请求”。若不做全局聚合(或按一致性哈希把 “计量职责” 也分片),各节点的 “已用容量” 会漂移,上限形同虚设。解决:把计数与路由绑定到同一个 owner,或用带版本的近似计数。
Q5 什么时候不该用有界负载?
▸ 答案:节点数很少(< 10)且 key 分布均匀时,普通一致性哈希 + 虚拟节点已够用,引入容量检查只是增加复杂度;此外,如果请求可以 “任意节点处理”(如无状态读 + 缓存),直接随机/轮询 + 客户端重试更简单。
Q7 本机实测中 “外溢 21.9%” 意味着什么代价?
▸ 答案:意味着约 1/5 的请求在路由时要多探测一跳以上:每次探测都是一次哈希环查找(O(log N)),尾延迟与 CPU 都会上升;同时节点恢复后这些外溢 key 需要迁回,产生额外迁移量。所以 ε 不是越小越好——ε=0.25 时外溢已超 20%,再调小会更糟。