一致性哈希减少了迁移,没有解决热点
缓存集群从 10 台扩到 11 台,按
hash(key) % N分片。扩容那一刻,命中率几乎掉到零,所有请求打到了数据库上。改成一致性哈希之后,扩容平稳了,可某一台缓存的 CPU 总是比其他的高一倍;后来又发现,一个明星用户的主页请求能把它所在的那台单独压垮。
一致性哈希是分布式系统里最常被提到的算法之一,它解决的是一个很具体的问题:节点数量变化时,尽量少地移动数据。它不直接解决负载均衡,更不解决热点。很多问题来自把这三件事混为一谈。
本文用 10 万个键,在 10 个节点上比较取模、哈希环、虚拟节点、有界负载四种做法,再加入第 11 个节点,统计负载分布和迁移比例;最后放一个占 20% 请求的热点键。所有计算都是确定性的,实验见文末。
一、取模:负载很均匀,扩容几乎全部迁移
取模:10 个节点 最多 10242、最少 9723,最多是平均的 1.02 倍;加入第 11 个节点后迁移 90.8%hash(key) % N 分布非常均匀,问题在 N 变化的时候:除数从 10 变成 11,绝大多数键的余数都变了,90.8% 的键换了节点。对缓存来说,这意味着扩容瞬间几乎所有缓存都失效;对存储来说,意味着几乎所有数据都要搬家。
二、哈希环:迁移少了,负载不均了
哈希环(每个节点 1 个点):最多 22569、最少 577,最多是平均的 2.26 倍;加节点迁移 12.2%(理想值 9.1%)哈希环把节点和键都哈希到同一个环上,每个键归属于顺时针方向的第一个节点。加入一个节点时,只有它和前一个节点之间的那段键会移动过来,理想情况下是 1/11,约 9.1%。
但每个节点只在环上占一个点时,各节点负责的弧长完全由哈希值的随机位置决定,差别非常大:最多的节点是平均的 2.26 倍,最少的只有 577 个键。开头那台 CPU 总是高一倍的缓存,就是这个原因。
三、虚拟节点:用更多的点换均匀
虚拟节点 10 个:最多是平均的 1.46 倍;加节点迁移 7.2%
虚拟节点 100 个:最多是平均的 1.19 倍;加节点迁移 8.3%
虚拟节点 1000 个:最多是平均的 1.05 倍;加节点迁移 9.4%让每个物理节点在环上占多个点(虚拟节点),弧长的随机差异被平均掉,负载越来越接近均匀。代价是环更大(查找需要在更多点中二分)、节点加入时要计算更多的位置。迁移比例都在理想值 9.1% 附近波动,与负载是否均匀有关:新节点分到的份额越接近 1/11,迁移量越接近理想值。
虚拟节点还有一个用途:给配置不同的机器分配不同数量的虚拟节点,按容量分配负载。
四、有界负载:直接限制上限
虚拟节点 10 个 + 有界负载 c=1.25:最多是平均的 1.25 倍;加节点迁移 9.2%
虚拟节点 10 个 + 有界负载 c=1.10:最多是平均的 1.10 倍;加节点迁移 12.9%有界负载的一致性哈希给每个节点设一个容量上限(平均负载的 c 倍),键落到已满的节点时,顺着环找下一个未满的节点。虚拟节点不多时,它能把最大负载直接压到 c 倍以内;c 越小越均匀,但越多的键被挤到「不是自己首选」的节点上,节点变化时连锁移动的键也越多(c=1.10 时迁移 12.9%)。
五、热点:所有这些都按键的个数均衡
一个键占 20% 的请求:它所在节点承担 27.7% 的请求,是平均的 2.77 倍;虚拟节点和有界负载按键的个数均衡,挡不住单个键的热度前面所有的做法,均衡的都是键的个数。但请求不是均匀分布在键上的:一个明星用户、一个爆款商品、一个热门话题,单个键就能占到总请求的相当一部分。一个键只能落在一个节点上(或者一组副本上),无论环怎么设计,那个节点都要承担它的全部请求。
热点要用别的办法:
- 本地缓存:在调用方进程内缓存热点键,大部分请求不再到达分片节点。
- 拆分热点键:把一个键复制成多个副本键(
key#1…key#8),读请求随机选一个,分散到多个节点;写入时要更新所有副本。 - 热点探测:统计访问频率,自动对超过阈值的键启用上面两种做法。
- 限流:保护那个节点不被压垮,至少不影响其他键。
热点在一个节点内部的表现(例如锁竞争、单线程瓶颈)还需要单独处理,Redis 的单线程模型下尤其明显。
六、怎样选
| 需求 | 做法 |
|---|---|
| 节点数量固定、几乎不变 | 取模最简单,分布也最均匀 |
| 节点会增减,要少迁移 | 一致性哈希 + 足够多的虚拟节点(几百个) |
| 节点容量不同 | 按容量分配虚拟节点数量 |
| 严格限制单节点负载 | 有界负载,接受更多的迁移 |
| 单个键的请求过多 | 本地缓存、拆分热点键、限流,与分片方式无关 |
另一个常被忽略的选择是预分片:先把数据分成固定数量的槽(例如 Redis Cluster 的 16384 个),再把槽分配给节点。扩容就是移动一部分槽,迁移量可控,分配也可以手动调整。它把「键到槽」和「槽到节点」分成两层,比一致性哈希更容易运维。
七、检查清单
- 先确认要解决的是迁移、负载均衡还是热点,三者用不同的办法。
- 使用一致性哈希时配置足够多的虚拟节点,并检查实际的负载分布。
- 需要严格上限时考虑有界负载,评估它带来的额外迁移。
- 单独监控每个键的访问频率,热点用本地缓存、拆分或限流处理。
- 节点数量变化频繁、需要人工控制时,考虑预分片。
配套实验
- codesphere-labs/distributed/consistent-hashing:JDK 25.0.4.1:10 万个键在取模、哈希环、虚拟节点、有界负载下的负载分布与扩容迁移,以及一个占 20% 请求的热点键(验证记录)
参考资料
- David Karger 等,Consistent Hashing and Random Trees
- Vahab Mirrokni、Mikkel Thorup、Morteza Zadimoghaddam,Consistent Hashing with Bounded Loads
- Redis 文档:Scale with Redis Cluster(hash slot)