bigywater.tech
微服务 · 19

一致性哈希:节点增减时有多少 key 被重新映射,与手写实现

节点增减时有多少 key 要重新映射?虚拟节点数怎么影响均匀程度?nginx 的 hash 指令加 consistent 之后真实差多少?这篇用 100000 个 key 的环和 nginx 的 reload 量出来。

90.8% 对 9.4%
加一个节点(10 → 11)时,取模与一致性哈希环(1000 个虚拟节点)归属变了的 key 的比例
980 对 0
nginx 去掉后端 D 时,普通 hash 与 hash consistent 下,原本不在 D 上却被搬走的 key 数(2000 个 key)
2.26 → 1.05
环上虚拟节点从 1 个增加到 1000 个时,最忙节点的负载占平均的倍数

取模 vs 环

实测 · 本机执行输出
  算法     | vnodes | 加 1 个节点:归属变了 | 去掉 1 个节点:归属变了 | 10 个节点时的负载不均(最忙 ÷ 平均)
  -------+--------+--------------+---------------+----------------------
  取模     | -      | 90.8%        | 89.8%         | 1.01                 
  一致性哈希环 | 1      | 12.1%        | 21.8%         | 2.26                 
  一致性哈希环 | 10     | 7.2%         | 13.7%         | 1.44                 
  一致性哈希环 | 100    | 8.2%         | 10.2%         | 1.20                 
  一致性哈希环 | 1000   | 9.4%         | 9.9%          | 1.05                 

取模加减一个节点几乎所有 key 都搬;环只搬 1/N;虚拟节点越多越均匀。

nginx 的 consistent

实测 · 本机执行输出
  指令                 | 4 个后端时 A / B / C / D 各收到 | 加 E:归属变了 | 其中搬到了新节点 E 的占比 | 去掉 D:归属变了 | 去掉 D 时原本不在 D 上却被搬走的 key 数
  -------------------+--------------------------+----------+----------------+-----------+--------------------------
  普通 hash            | 499 / 501 / 501 / 499    | 79.7%    | 26%            | 74.0%     | 980                      
  consistent(ketama) | 477 / 535 / 458 / 530    | 23.8%    | 100%           | 26.5%     | 0                        
  理想值:加第 5 个节点只需搬 1/5 = 20% 的 key(都搬到 E);去掉 D 只需搬原本在 D 上的 key(约 25%),其余 key 不动

普通 hash 加一个后端时 key 在老节点之间乱搬;ketama 只搬向新节点,去掉一个后端时只搬它上面的 key。

权重与热点

实测 · 本机执行输出
  按权重分:n0 = 24.5% (理论 25.0%);其余 9 个节点各自: 7.7% / 8.1% / 9.5% / 8.1% / 10.4% / 7.9% / 7.8% / 7.9% / 8.1%
  热点 key 落在 n0(权重 3):它的负载占全部流量的 32.0%;按权重本来应该占 25.0%。其余各节点的负载占比: n0=32.0% / n1=6.9% / n2=7.3% / n3=8.7% / n4=7.3% / n5=9.3% / n6=7.1% / n7=7.0% / n8=7.1% / n9=7.3%

权重通过虚拟节点数实现;热点 key 落到哪个节点,哪个节点就多承担一块,一致性哈希不处理热度。

分步演示

一致性哈希
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。手写的环用 md5 的前 8 字节作哈希,每个节点放 vnodes 个虚拟节点,bisect 找顺时针第一个;nginx 是 Homebrew 的 1.31.6,用 reload 换后端列表(单 worker 进程);nginx 的 ketama 哈希函数与我的 md5 环不同,所以不比较二者的具体归属,只比较搬家比例
    • 没有验证:nginx ketama 的虚拟节点数与权重的关系;一致性哈希的有界负载变体(bounded loads);Rendezvous(HRW)哈希与 Maglev 哈希;一次 reload 中新旧 worker 同时存在的过渡期内请求的归属;更多节点(几十上百个)的情形
    京ICP备2026067502号-1京公网安备11010502063487号