概述
一致性 hash 是传统 hash 算法的增强版。
多用于分布式数据存储场景,在集群节点数量发生变化时,提升集群适应变化的能力。
传统hash
假设当前服务集群中存在 3 个节点:Node-A,Node-B, Node-C;而客户端存在 Key1,Key2,Key3 需要映射到对应的服务节点。
传统 hash 算法思路:
- 先计算 key 对应的 hash 值
- 将 hash 值和服务节点的数量取模,算出对应节点的下标,即 Hash(Key) % NodeSize
如下图,通过传统 hash 计算出的映射关系:
- Key1 -> Node-A
- Key2 -> Node-B
- Key3 -> Node-C
如果 Node-C 节点宕机了,Hash(Key) % NodeSize 公式的取模对象发生变化,最终可能导致 Key1,Key2 的映射到的服务节点都发生变化(Key3 肯定会改变)。
原本 Key1 映射到 Node-A 变为映射到 Node-B,因为数据之前存储在 Node-A,则导致 Key1 无法正常命中数据;Key2,Key3 … KeyN 都可能出现这种情况。
传统 hash算法的局限性主要体现在:
- 节点数量发生变化,导致 key -> 节点的映射关系发生变化,最终导致数据存储服务不可用(之前存储的 key 无法正常命中数据)
- 节点数量发生变化,整体数据 Rehash 的成本较高
而一致性 hash 算法则是将这种因节点数量变化所需要花费的调整成本,降至最低。
一致性hash
一致性 hash 引入了哈希环的概念,核心思路:
- 规定了一个哈希环,环的元素由 [0, 2^32 -1] 范围的整数组成
- 将 key,服务节点通过计算映射到哈希环上
- 顺时针方向为 key 寻找相邻的第一台服务节点,完成 key -> node 的关系映射
通过上述哈希环计算出来的映射关系:
- Key1 -> Node-A
- Key2 -> Node-B
- Key3 -> Node-C
如果此时 Node-C 宕机了,则受影响的数据范围仅仅是 Node-B 到 Node-C 之间的数据,即 Key3 改为映射到 Node-A。
对于节点新增的场景也同理。
虚拟节点
上述的一致性 hash 确实能够降低节点数量变化对集群整体造成的影响,但存在数据倾斜问题。
假设集群中只存在两个节点 Node-A,Node-B且它们如下图分布于哈希环上。
即使 hash 算法足够平衡,但明显 Node-A -> Node-B 的这段区间长度更大,因此大部分 Key 会落在这个区间,最终导致大量数据都倾斜在了 Node-B 存储。
而虚拟节点的引入,能够最大程度使节点数据分布均匀,解决数据倾斜的问题。它的核心:
- 将一个物理节点分化成多个虚拟节点
- 将虚拟节点映射到哈希环上
- 当 key 命中虚拟节点后,通过虚拟节点找到其所属的物理节点
如下图:
可以看到 Node-A,Node-B 分别分化出了两个虚拟节点,使得数据分布更加平衡。
另外通过虚拟节点,在新增节点的情况下也使得新机器能够帮助集群承担更多的数据压力。
算法实现
使用 TreeMap 存储哈希环上数据,key 为 hash 值,value 则对应服务节点的信息。
采用 FNV-Hash 算法,这种算法的特点是:能快速hash大量数据并保持较小的冲突率。
它的高度分散使它适用于hash一些非常相近的字符串,比如URL,hostname,文件名,text,IP地址等。
总结
本文主要讲述了:
- 传统 hash 算法在分布式环境下的局限性
- 一致性 hash 算法的实现思路,以及最终解决的问题
- 一致性 hash 算法的代码实现
希望通过这篇文章,能够对一致性 hash 算法有一个比较清晰的认识。
另外像 Jedis,RocketMQ 等开源项目中都有相关的算法实现,理解了原理再去阅读源码,可以事半功倍。