一篇读懂一致性哈希:扩容时只搬 K/N 个 key 的艺术

分布式系统里最平凡也最常见的动作是加机器和减机器,而它最常触发的事故叫「缓存雪崩」:集群从 10 台扩到 11 台,哈希公式里的模数一变,绝大多数缓存 key 突然映射到了新的机器——可新机器上什么都没有,于是全部请求涌向数据库。数据没丢、代码没错,只是一次例行扩容,就把缓存打穿了。一致性哈希(consistent hashing)就是为了治这个病而生:它能保证节点增减时,平均只有约 K/N 的 key 需要搬家(K 是 key 总数,N 是节点数),而不是「几乎全部」。本文把它讲透:环模型怎么转、数据倾斜怎么治、它的家族变体有哪些,以及工程落地的边界。

一致性哈希手绘信息图:取模病灶、环模型、虚拟节点、家族变体与应用系统

一、先看清病灶:取模哈希把「全局」绑上了「局部」

最直觉的分布式哈希是取模:hash(key) % N,N 是节点数。它在集群规模稳定时工作得很好,问题出在 N 变化的那一刻。设一个 key 的哈希值是 100:10 台节点时落在 100 % 10 = 0 号机;扩到 11 台,变成 100 % 11 = 1 号机——换机了。这不是个例:对绝大多数 key 来说,模数从 10 变 11 都会改变结果。也就是说,一台机器的增减,惩罚的是全部分片——缓存几乎全 miss,数据库瞬间接住所有流量;如果是存储分片,那就是一场全量数据迁移。

病灶的本质,是取模把「key 到节点的映射」和「节点总数」这个全局变量绑死了:任何局部变化(加一台、减一台)都会改写全局。这就是为什么这类事故最阴险:代码一行没改、数据一个没丢、容量明明变大了,系统反而雪崩——没有人把「扩容会改变映射关系」写进事故预案。顺便说一句,有人会想「那我每次扩容翻倍不就行了」——翻倍时确实只有约一半 key 会搬家,但「一半」对缓存依然是灾难,而且翻倍的容量粒度对成本是巨大浪费。要治它,就得让映射关系与节点总数解耦——单节点的变化只影响自己附近的那一小片。这就是一致性哈希的全部动机。

二、环模型:把服务器和 key 撒到同一个圆上

一致性哈希的做法分两步。第一步,把哈希空间想成一个首尾相接的环(通常用 32 位哈希值,范围 0 到 2^32−1,头尾相接)。第二步,服务器和 key 都用同一个哈希函数撒到环上:每台服务器按其地址(如 IP)哈希后占据环上一个位置;每个 key 哈希后也落在环上,归属规则只有一句话——沿顺时针方向找到的第一个服务器,就是它的归宿。

妙处立刻显现:加一台服务器,它只接管环上「它到前一台服务器」之间那段弧的 key,其余 key 的归属完全不变;删一台服务器,只有它名下的 key 顺延给下一位。平均下来,一次节点变动只影响 K/N 个 key——这正是我们要的性质,而且它对任何一台节点的增删都成立,不需要预先知道集群会怎么变化。

实现上,环用有序结构维护(平衡二叉搜索树或有序数组),key 到达时做一次二分查找就能定位后继节点,单次查找 O(log N)。代价也在此:相比普通哈希表的 O(1),一致性哈希的查找慢了一个对数因子——这是「少搬数据」买下的单,对每秒百万次的路由查询而言,一次二分不过十几次比较,大多数场景极其划算。

这个思想 1997 年由 MIT 的 Karger 等人在 STOC 论文上正式发表,初衷是解决 Web 缓存在服务器动态增减时的请求分配问题;论文的合作者 Daniel Lewin 与 F. Thomson Leighton 随后在 1998 年创立了 Akamai,把它做成了全球最大 CDN 的基石。有趣的是, Teradata 1986 年的分布式数据库里就出现过同样的技术,只是当时没有这个名字——好思想被重新发明,在工程史上从来不稀奇。

三、数据倾斜与虚拟节点:给每个节点撒一把影子

朴素环模型有一个现实的坑:哈希函数是均匀的,但节点数少时,它们在环上的位置纯属随机——可能两台节点几乎贴在一起,也可能出现一段没有任何服务器的长弧。结果是一台节点可能分到远超平均的 key,负载严重倾斜;更糟的是故障场景:一台节点下线后,它名下的 key 全部砸给环上的下一位,那位瞬间负载翻倍。

解法是虚拟节点(virtual node):让每个物理节点在环上撒几十上百个「影子」——同一个物理机器哈希出许多个位置,每个位置都指向它。影子足够多时,大数定律开始工作:每台物理节点在环上占有的弧长趋于均匀,key 的分配也随之均衡。它还有第二层作用——权重:给性能强的机器撒更多影子,它就自然承接更多 key,异构集群的负载均衡顺带解决。第三层作用常被忽略:故障时的负载平滑。没有虚拟节点时,一台节点下线,它整段弧的 key 全部压给下一位邻居——邻居瞬间双倍负载,可能引发连锁过载;有几百个虚拟节点时,故障节点的 key 会分散归还给环上的许多不同物理节点,谁都不会被压垮。Amazon 的 Dynamo 论文把虚拟节点变成了工业界标配,工程实践中常见的配置是每个物理节点上百个影子(早期版本约 100–200 个,按机器性能调整);Cassandra 等系统沿用至今。

顺便厘清一个容易混淆的替代方案:把哈希空间切成固定数量的「槽」(比如 16384 个,Redis Cluster 的做法),槽先哈希定死、再把槽映射到物理节点。它同样做到「只搬槽不搬全局」,本质上是把虚拟节点思想推到极致——虚拟节点数量固定、由中心化管理映射。代价是需要一层槽到节点的映射元数据并保证各处视图一致;换来的是迁移单位固定、扩缩容的搬运清单可以精确计算。两种路线在工业界都有一线系统背书,选型时按「愿不愿意维护元数据服务」来分即可。

四、家族与变体:不止一种「一致性」

一致性哈希是个家族。Rendezvous 哈希(也叫最高随机权重法,1996 年提出)甚至更简单:每个 key 对所有候选节点分别算一个哈希,取得分最高的节点。它不需要维护环结构、天然支持节点权重,代价是每次分配都要遍历全部节点——节点数不大时非常好用;有趣的是,一致性哈希在数学上可以证明是它的特例。

Jump Consistent Hash(Google 2014 年论文)走了另一个极端:只需几行代码、零内存,直接由 key 和桶数算出归属,速度极快且分布均匀,适合 key 单调增加的场景;代价是不支持给节点配任意权重、也查不出「key 在哪个桶」的反向映射。工程选型的口诀大致是:要环上的区间语义(如分片路由、副本放置)用经典环加虚拟节点;节点数少、要权重、怕维护,用 rendezvous;超大 key 量、桶只增不减,用 jump hash。至于谁在用:Dynamo、Cassandra、Couchbase、OpenStack Swift、MinIO、Discord、Akamai CDN——名单本身就是可靠性背书。

五、工程落地清单

  1. 环用有序结构,别线性扫。平衡树或有序数组加二分,查找 O(log N);虚拟节点几百上千个时线性扫描会成为瓶颈。
  2. 虚拟节点数做权衡。太少则依旧倾斜,太多则环表占内存、变更时元数据同步变多;从「每物理节点 100+」起步按观测调整。
  3. 一致性哈希只管「少搬家」,不管热点。单个爆款 key 再均匀的哈希也会把流量全砸到一台节点——热点 key 要靠复制、本地缓存或 key 打散单独治理,别指望换哈希算法。
  4. 节点变更要过广播。环的视图必须在所有客户端一致,否则同一个 key 会被路由到不同节点;生产实现(如 Cassandra 的 gossip、配置中心推送)都花大力气解决「环视图的一致性」这件事本身。
  5. 哈希函数要分布均匀且稳定。换哈希函数等于全部重新分片;用成熟函数(如 MurmurHash),别用 CRC 这种分布特性不合场景的。
  6. 别忘了副本与环的关系。Dynamo 类系统取「顺时针往下数 N 个不同物理节点」作为偏好列表做 N 副本——环模型顺带把副本放置问题也解决了,这是它比分片映射表更受欢迎的深层原因:路由、迁移、副本三件事共用同一个环。

结语

一致性哈希的价值不在算法多精巧——它的规则一句话就能说完——而在它把「分布式系统的变化」从全局事件降级成了局部事件。加一台机器,天下只动 K/N 之一;这个性质让缓存扩容不再是事故、存储分片不再是迁移工程,也让后来者(Dynamo、Cassandra 一代系统)敢于把「永远可写」做成设计目标。一句话总结它的思想内核:在哈希这件事上,随机撒点加有序查找,胜过精确公式加全局重算。下次扩容前看一眼哈希方案:如果你的公式里有 % N,现在就知道事故该怎么预防了。

参考资料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?