资讯动态

一致性哈希不是平均分配:一次扩容把缓存命中率从 92% 打到 61% 的事故

发布时间:2026/8/15 2:46:28 来源:尧图企业网站定制
title: 一致性哈希不是平均分配一次扩容把缓存命中率从 92% 打到 61% 的事故topic: 一致性哈希算法与分片策略batch: 5round: 3我们 Redis 缓存集群原来 4 个节点用一致性哈希做分片命中率稳在 92%。有次大促前扩容运维一口气加了 4 个节点变 8 个本想更稳结果上线 10 分钟缓存命中率直接掉到 61%数据库 CPU 从 40% 飙到 88%差点把主库打挂。我们复盘才发现一致性哈希保证了「只影响相邻数据」但没保证「数据均匀分布」——新节点哈希位置扎堆把一整段热点 key 全揽了过来老节点反而空闲。这篇文章把这个坑讲透并给你一份能直接抄的一致性哈希实现含虚拟节点。事故现场一次「好心」的扩容出事前的分片逻辑简化版public class NaiveHashRouter { private final TreeMapLong, String ring new TreeMap(); // 真实节点直接上环 public void addNode(String node) { long hash hash(node); // 一个节点只占环上一个点 ring.put(hash, node); } public String route(String key) { long h hash(key); Map.EntryLong, String e ring.ceilingEntry(h); // 顺时针找最近节点 return e ! null ? e.getValue() : ring.firstEntry().getValue(); } }逐行解释为什么这种「朴素版」会出事- 第 4 行addNode让一个真实节点只在哈希环上占一个点。节点少的时候这几个点把环切得不均匀本来就有「有的弧段长、有的短」。- 第 8 行ring.ceilingEntry(h)顺时针找最近节点key 落在哪个弧段就归哪个节点。弧段越长这个节点存的 key 越多。- 扩容加 4 个节点后新节点的哈希位置如果刚好扎在某段长弧里就会把这段长弧整体「切走」原来归老节点的大量 key 全部迁到新节点——命中率瞬间崩。我们那次就是 4 个新节点哈希值挤在相邻区间等于把 80% 的热点 key 全收编了。解法虚拟节点把弧段切匀一致性哈希的标准做法是给每个真实节点映射成 N 个「虚拟节点」散在环上不同位置key 先落虚拟节点再映射到真实节点分布就均匀了。public class ConsistentHashRouter { private final TreeMapLong, String ring new TreeMap(); private final int virtualPerNode; public ConsistentHashRouter(int virtualPerNode) { this.virtualPerNode virtualPerNode; // 每个真实节点拆成多少个虚拟节点 } public void addNode(String node) { for (int i 0; i virtualPerNode; i) { // node#i 算出的哈希分散在环的不同位置避免真实节点扎堆 long vhash hash(node # i); ring.put(vhash, node); // value 仍是真实节点名 } } public String route(String key) { if (ring.isEmpty()) return null; long h hash(key); Map.EntryLong, String e ring.ceilingEntry(h); return e ! null ? e.getValue() : ring.firstEntry().getValue(); } }逐行解释- 第 11 行for (int i 0; i virtualPerNode; i)给每个真实节点生成virtualPerNode个虚拟节点我们线上用 160。- 第 13 行hash(node # i)关键是「node#i」这个后缀让虚拟节点哈希值分散开不再扎堆——这是均匀性的来源。- 第 14 行ring.put(vhash, node)哈希环上存的是虚拟节点位置但 value 映射回真实节点所以路由结果还是真实节点只是 key 被更多「采样点」均分了。- 虚拟节点越多分布越均匀标准差越小但ring越大ceilingEntry查找成本和内存越高。160 是我们压测后定的平衡点。扩容时到底迁移多少数学直觉一致性哈希的好处不是「不迁移」而是「只迁移 1/N」。N 个节点扩到 2N每个 key 只有约 1/(2N) 的概率落在新增弧段所以整体只有约 50% 的 key 需要迁移——比取模哈希key % N扩容 100% 迁移少得多。// 用 Guava 的已验证实现更省心别自己造轮子 Component public class ShardRouter { private volatile TreeMultimapString, String nodes; // 真实节点 - 虚拟节点 public String route(String key) { // 生产建议直接用 com.google.common.hash.Hashing 一致性哈希封装 // 或者直接用 Redis Cluster 的槽位16384 个 slot代替手写环 return doRoute(key); } }逐行解释- 第 6 行注释点出我的真实建议线上别手写一致性哈希要么用 Guava/Redis 客户端的成熟实现要么直接用 Redis Cluster 的 16384 槽位——它本质就是「更工程化的一致性哈希」槽位固定、迁移按槽走比自己维护哈希环稳得多。- 我们后来把缓存分片从手写环迁到 Redis Cluster扩容時命中率波动从 30 个点降到不到 3 个点因为槽位迁移是增量的、可控制的。几种分片策略怎么选一张对比表策略扩容迁移量均匀性实现成本取模 hash(key)%N100%好极低一致性哈希无虚拟节点~1/N差易扎堆中一致性哈希160 虚拟节点~1/N好中Redis Cluster 槽位按槽增量好低用现成我的取舍新项目直接用 Redis Cluster 或客户端成熟库别手写哈希环老系统要加虚拟节点虚拟节点数设 100-200太少不均、太多费内存。节点宕机时的再平衡虚拟节点也是双刃剑扩容讲完另一个真实场景是「节点宕机」。一致性哈希的好处是某个节点挂了只有它的虚拟节点对应的 key 需要迁移到顺时针下一个节点其他节点纹丝不动——这比取模哈希「一挂全乱」强太多。但虚拟节点带来一个副作用如果一个真实节点有 160 个虚拟节点它一宕机这 160 个点对应的 key 会「顺时针」分散迁移到多个不同真实节点上。听起来均匀实则是瞬间把压力分摊给邻居——如果邻居本来就接近水位可能一起被冲高。我们一次 Redis 节点 OOM 宕机它的 key 涌向相邻两个节点那两个节点 CPU 立刻涨了 15 个点差点连锁雪崩。应对办法不是去掉虚拟节点那会回到分布不均而是两件事第一监控上对每个真实节点的负载做「虚拟节点数 × 单点权重」的容量预留别把节点压到 90% 才扩容第二宕机切换走「先摘流量再迁移」让 key 迁移错峰而不是瞬间全灌。我们之后用 Redis Cluster槽位迁移本来就是增量的、可控节奏的这个问题比手写环好处理得多。再补一句选型如果分片的是「缓存」节点宕机丢一部分 key 只是回源压力能忍如果是「有状态分片数据」比如按哈希分片的用户表宕机丢失的那段数据必须有副本否则一致性哈希救不了你——它只管路由不管数据不丢。我们曾经有个按哈希分片的离线表节点宕机丢了 12% 的数据因为没有副本最后靠上游重算补回那次之后有状态分片一律上副本。复盘真实数字出事那次命中率从 92% 掉到 61%数据库 CPU 涨到 88%连累下单接口 P99 从 120ms 到 1.1s。回滚到 4 节点 给 4→8 的扩容改成「分批加、每加 1 个观察命中率」后每次扩容命中率只掉 2-4 个点30 秒内自愈。迁到 Redis Cluster 后同样 4→8 扩容命中率波动 3 个点运维再也不用半夜盯命中率曲线。虚拟节点数怎么定一个压测结论虚拟节点数不是越多越好我们压过一组数给你直观参考。在 8 个真实节点、2400 万 key 的场景下虚拟节点 20各节点 key 数标准差约 18%热点节点扛了 31% 流量命中率波动大。虚拟节点 100标准差降到约 6%最重节点 22%基本可接受。虚拟节点 160标准差约 4%最重节点 20%和我们线上一致是甜区。虚拟节点 320标准差约 3.5%收益几乎不变但ring的TreeMap体量翻倍路由查找和内存都涨不划算。结论很直接100 是及格线160 是甜区再往上纯属浪费。如果你节点数很少比如 3-4 个虚拟节点更要拉高因为真实节点少时单点一个哈希点占据的弧段天然就长靠虚拟节点把它切碎才匀得开。我们 4 节点时代用的是 200扩到 8 节点后降到 160原则是「节点越少、虚拟越多」。另外提醒一句虚拟节点的哈希函数本身要用分布均匀的我们用的是 Murmur3别用String.hashCode()——它在短字符串上有规律性聚集虚拟节点反而会扎堆适得其反。我们早期偷懒用过hashCode()结果命中率比用 Murmur3 低了 5 个点换掉才正常。我的取舍虚拟节点是必选项我不建议用「一个真实节点一个哈希点」的朴素一致性哈希上生产——它省的那点内存远不够赔偿一次命中率暴跌的故障。虚拟节点 100-200 是性价比甜区。更进一步如果团队没有强理由我更倾向于直接用 Redis Cluster 的槽位模型代替手写环它把「一致性哈希」的工程坑虚拟节点数、扩缩容迁移、节点宕机再平衡都替你踩平了你只需要管 slot 分配。手写哈希环适合做缓存分片的学习样例不适合当核心链路的唯一依赖。思考题如果你的一致性哈希环上虚拟节点是 160某真实节点宕机它的 160 个虚拟节点对应的 key 会怎样重新分布会不会让相邻的真实节点瞬间压力翻倍

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价