用 hash(key) % N 给 key 选节点,N 一旦变化,几乎全部 key 的归属都要重算,扩缩容随即演变成缓存雪崩。一致性哈希(Consistent Hashing)把节点和数据放进同一个环形哈希空间,按顺时针方向寻找负责人,把节点增减的影响压缩到环上相邻的一段区间。本文按”取模之殇、哈希环、虚拟节点、迁移量、真实应用”的顺序展开。

普通取模哈希:节点一动,全盘重排

负载均衡和分布式缓存面对的是同一个问题:建立 key 到节点的稳定映射。最直觉的写法是哈希取模:

node = hash(key) % N        # N 为节点数

公式简单,分布也够均匀,但它把映射关系绑死在”节点总数 N”这个变量上。假设原本 4 台缓存服务器,其中一台宕机,N 从 4 变成 3,对同一个 key 来说 hash(key) % 4 和 hash(key) % 3 几乎必然不同。可以算一笔账:N 减一时,平均有 (N-1)/N 比例的 key 需要换节点,节点越多这个比例越接近 100%。

换节点的后果在缓存场景里被无限放大:成片的 key 同时 miss,请求穿透到后端数据库,大促前扩一次容反而可能把数据库压垮。故障宕机同理——机器只是坏了一台,效果却像整个缓存层被清空。生产系统真正需要的是”扩缩容只动一小部分数据”,一致性哈希正是为此设计的。

哈希环:对固定值取模,顺时针定位

一致性哈希于 1997 年由麻省理工学院的学者提出,核心改动只有一处:不再对节点数取模,而是对一个固定的哈希空间取模。这个空间通常取 [0, 2^32-1],首尾相接构成一个环:

位置 = hash(key) % 2^32     # 对固定值 2^32 取模

节点也用同样的规则上环——一般对 IP 或主机名做哈希。于是环上既有节点也有 key,归属规则只有一句话:从 key 的位置出发,沿顺时针方向遇到的第一个节点,就是它负责的节点。

            NodeA(100)
/ \
key1(50) key3(2500)
| |
NodeD(3000) NodeB(1200)
\ /
NodeC(2000)

key1 顺时针先到 NodeA -> 归 NodeA
key3 顺时针先到 NodeD -> 归 NodeD

每个节点实际负责的是”自己的逆时针邻居到自己”这一段弧上的所有 key。映射规则里彻底没有了 N,这是后面一切性质的来源。

增删节点:只动相邻区间

把映射和节点数解耦之后,增删的影响范围就只剩环上的相邻区间:

  • 下线:NodeB 宕机,它负责的弧段顺延给顺时针方向的下一个节点,其余区间的归属纹丝不动。
  • 上线:在 NodeA 与 NodeB 之间插入新节点,只有落在新节点与 NodeA 之间的那段 key 从 NodeB 手里移交过去,理想情况下迁移量约为环上 1/(N+1) 的区间。

对比取模哈希接近 100% 的重映射,一致性哈希在均匀情况下把单次增删的迁移量压到约 1/N。节点越多,单次变动的代价越小——这正是缓存集群敢在线扩缩容的底气。

数据倾斜与虚拟节点

上面的 1/N 是理想值。哈希函数是随机的,节点在环上的落点并不保证均匀,尤其物理节点少的时候,很可能挤作一团:某台机器分到的弧段占了大半个环,隔壁机器却几乎无事可做。这就是数据倾斜。

倾斜还会在故障时二次伤人:热点节点宕机,它囤积的成段数据会整体砸给唯一的顺时针后继,后继可能直接被打崩。

虚拟节点:一台机器,环上百个分身

工程解法是虚拟节点(Virtual Node):一台物理节点不再只占一个位置,而是用 IP#1、IP#2 之类的标识算出上百个分身,散布在环的各处;key 先定位到分身,再归入背后的物理机。

虚拟节点一举解决三件事:

  1. 拉平分布:分身足够多时,大数定律开始起作用,每台物理机分到的弧段总长趋于平均。常见配置是每个物理节点 100~200 个虚拟节点。
  2. 分散故障:物理机下线相当于环上多个点同时消失,它的数据被拆散、均匀地移交给多台不同的邻居,而不是压垮单个后继。
  3. 支持异构:给性能强的机器多分虚拟节点(权重),它自然承担更多流量,且不必修改归属规则。

代价也不是没有:环上点位变多,元数据和查找成本略增,虚拟节点数需要压测调优。

真实系统都在怎么用

  • Dynamo:亚马逊的分布式键值存储在 2007 年的论文里把一致性哈希推向工业界视野,并用虚拟节点处理机器异构与负载均衡,是这一思想的标志性落地。
  • Memcached 客户端分片:Memcached 服务端彼此无感知,由客户端(ketama 风格的实现)在本地维护哈希环选择服务器,至今仍是多节点 Memcached 部署的主流姿势。
  • 分库分表中间件:以用户 ID 之类的分片键走一致性哈希定位库表,扩容加库时只需迁移环上相邻区间的数据,避免全量搬表。
  • 负载均衡与会话亲和:Nginx 等网关的一致性哈希模块让同一来源的请求尽量落到固定后端,缓存命中率随之提升。

顺带澄清一个高频混淆:Redis Cluster 用的不是哈希环,而是 16384 个固定槽位(slot),key 对槽位数取模、槽位再成批分配给节点。思路与一致性哈希一脉相承——同样为了让扩容按小批次迁移——但归属规则不同,面试里别把两者画等号。

落地要点

  • 钉死哈希函数:哈希算法一旦更换,等于全体 key 重映射。MurMurHash、CRC32 这类非加密哈希是常见选择,上线后冻结版本。
  • 压测虚拟节点数:太少压不住倾斜,太多浪费内存和查找时间,结合节点规模实测后再定。
  • 一致性哈希管不了热点:它解决的是”扩缩容迁移量”,不是”某个 key 被打爆”。极端热 key 要靠本地缓存、请求合并或独立分片兜底。
  • 持续监控倾斜:盯各节点的 key 数量与 QPS 方差,而不仅是总量。

面试视角

一致性哈希解决了什么问题?

解决节点增删时的大规模重映射:取模哈希下迁移比例趋近 100%,哈希环把单次变动压缩到相邻区间、约 1/N。它不保证数据绝对均匀(靠虚拟节点逼近),更不是共识算法——它管”数据放哪”,不管”多副本怎么一致”。

为什么要虚拟节点?

物理节点少时哈希落点随机,弧段长短悬殊导致倾斜;一台机器映射成环上上百个分身后,分布被大数定律拉平,故障流量也分散给多个邻居接管。

小结

  • 取模哈希把映射绑在节点数 N 上,N 一变就近乎全量重排,缓存场景里等同雪崩。
  • 一致性哈希对固定的 2^32 空间取模,key 顺时针找第一个节点,增删只迁移相邻区间,单次约 1/N。
  • 虚拟节点用”一机多分身”拉平分布、分散故障、表达权重,是工程标配。
  • Dynamo、Memcached 客户端、分库分表中间件是三类典型落地;Redis Cluster 的 slot 是思路相近的另一条路。