一致性哈希扩缩容实测:取模与虚拟节点方案的数据迁移量对比
一致性哈希扩缩容实测:取模与虚拟节点方案的数据迁移量对比
一、扩一台机器,为什么 80% 的缓存都失效了
缓存集群从 4 台扩到 5 台,最直观的做法是 hash(key) % N。看起来只是多了一台机器,但 N 从 4 变成 5,几乎所有 key 的取模结果都变了。我用 20 万个 key 做了实测:迁移比例 80.01%,只有 20% 的 key 还留在原节点。
这意味着扩一台机器的瞬间,80% 的请求会 miss 并回源到数据库。如果回源 QPS 是平时的 5 倍,数据库大概率扛不住。所以扩缩容在缓存场景里从来不是"加机器",而是一次容量事件。
二、实验设置
- key 空间:200000 个形如
user:session:{i}的字符串 - 哈希:MD5 取 128 位整数(生产可用 crc32 / MurmurHash3,均匀性接近)
- 环结构:4 节点 × V 个虚拟节点,落点升序排列,查找用 bisect 二分
- 观测指标:迁移比例、各节点份额的标准差、极值比(最大份额/最小份额)
三、一致性哈希把迁移量压到 1/(N+1)
代码核心只有三行:算哈希、二分、取顺时针后继节点。
import bisect
def h(s):
return int(hashlib.md5(s.encode()).hexdigest(), 16)
def owner(key, hashes, owners):
i = bisect.bisect_right(hashes, h(key))
return owners[i % len(owners)]
扩容时新节点只从环上接管它前面那一段弧,其他节点的弧保持不变。理论上迁移量应当正好等于 1/5 = 20%,实测(V=200)是 19.32%,与理论值吻合。
四、虚拟节点数量才是真正的调参点
如果每个物理节点只放一个虚拟节点(V=1),环形空间被切成 4~5 段,段长由哈希随机决定,分布极不均衡:实测 5 个节点里最小的只拿到 0.05% 的 key,最大的拿到 72.77%,极值比 1413 倍。这种集群谈扩容没有意义,因为扩容之前就已经有一台机器在裸奔。
| 虚拟节点数 V | 扩容迁移比例 | 最小份额 | 最大份额 | 极值比 | 份额标准差 |
|---|---|---|---|---|---|
| 取模(无环) | 80.01% | — | — | — | — |
| 1 | 21.45% | 0.05% | 72.77% | 1413× | 0.270 |
| 50 | 25.11% | 16.15% | 24.28% | 1.50× | 0.028 |
| 200 | 19.32% | 17.36% | 21.87% | 1.26× | 0.016 |
| 1000 | 20.52% | 19.46% | 20.77% | 1.07× | 0.005 |
注意 V=50 的迁移比例 25.11% 反而高于理论值 20%。这不是测量误差,而是环本身的分布偏斜:新节点那段弧恰好偏长,接管了更多 key。虚拟节点越少,这种随机偏差越大。
五、V 不是越大越好
环上节点数是 N × V,查找复杂度 O(log(NV))。V 从 200 提到 1000,极值比只从 1.26 降到 1.07,收益明显递减;代价是环结构内存涨 5 倍,节点上下线时的重平衡计算也涨 5 倍。经验值:中小集群 V=100~200 足够,只有节点数上百、单机容量差异大的场景才需要 500 以上。
另一个容易被忽略的点是缩容比扩容更危险。下线一台机器时,它的全部 key 会瞬间压到顺时针下一个节点。实测 5→4 缩容、V=200 时迁移 21.87%,但这些 key 集中涌向一个节点,该节点瞬间要接住 1.2 倍流量。所以缩容必须配合限速迁移或灰度摘除。
六、生产落地的四个动作
- 变更前算清增量:迁移比例 × key 总量 = 回源峰值,提前为数据库留余量。
- 双读过渡:新旧环同时查,两边都 miss 才回源,避免单点抖动。
- 迁移限速:按 key 前缀分批搬迁,单批不超过集群容量的 5%。
- 监控口径:不只看命中率,还要看单节点份额,极值比超过 1.5 就该增加虚拟节点。
七、结论
取模在节点数不变时最省事,一旦扩缩容就要重建 80% 的映射;一致性哈希把迁移量锁在 1/(N+1),代价是必须用足够多的虚拟节点换取均匀性。真正决定集群能否平滑扩容的,不是"有没有用一致性哈希",而是虚拟节点数、迁移限速、双读过渡这三件事有没有一起做到位。
- 点赞
- 收藏
- 关注作者
评论(0)