在实际生产应用中,经常会设置多台服务器共同组成一个集成对外提供服务,为了确保合理的分配来自客户端的请求,我们会采取负载均衡的策略。例如采用「轮询」的方式让每个节点都能公平的接收到请求;采用「加权轮询」的方式让硬件配置高的节点承担更多的请求。
但是,在分布式系统的环境下,数据有可能是经过「水平切分」后放在不同的节点上的,每个节点存储的数据都是不同的,所以需要采取一种新的策略来保证请求能唯一准确地打到对应的节点上。
哈希算法
最容易想到的方式就是采取「哈希算法」,因为对同一个关键字进行哈希计算,每次计算得到的都是相同的值,这样就可以将某个 key 确定到一个节点上了,可以满足分布式系统的负载均衡需求。
哈希算法最简单的做法就是进行取模运算,假设分布式系统中有 3 个节点,基于hash(key) % 3
公式对数据进行了映射,如果客户端要获取指定 key 的数据,同样通过上述公式就可以定位到该节点,如果经过公式计算后得到的值是 0,就说明该 key 需要去第一个节点获取。
然而,当服务器节点增加或减少时,原有的映射将失效,大部分数据都需要重新映射到新的服务器节点上,这会导致大规模的数据迁移,影响系统性能和可用性。
一致性哈希算法
「一致性哈希算法(Consistent Hashing)」是一种用于分布式系统中数据分片和负载均衡的哈希算法。它解决了传统哈希算法带来的服务器节点增减时数据重新分布的问题,同时在保持负载均衡的情况下,尽量少地迁移数据。
一致性哈希算法也采用了取模运算,但与哈希算法不同的是,哈希算法是针对节点的数量进行取模运算,而一致性哈希算法是对2^32
进行取模运算,是一个固定的值。
我们可以把一致性哈希算法对2^32
进行取模运算的结果值组织成一个圆环,就像钟表一样,通过将哈希空间设计为一个环形结构,这个圆环也被称为哈希环。
为什么要选择对2^32进行取模运算呢?
- 哈希环的均匀性:
2^32
是一个很大的数值,取模运算可以将哈希值分散在整个哈希环上,使得数据在哈希环上的分布更均匀。这样可以保持节点负载均衡,减少数据倾斜的可能性; - 范围的大小:
2^32
等于 4294967296,这个范围足够大,可以容纳绝大多数哈希值。即使在非常大规模的系统中,仍然可以通过取模运算对节点进行充分的分布; - 效率的考虑:取模运算是一种简单高效的运算,可以在常数时间内完成,不会造成太大的计算开销;
- 可扩展性的考虑:当需要增加或删除服务器节点时,可以方便地调整哈希环的大小(例如 2^32)来适应节点数量的变化,而不会影响已有数据的映射关系
总之,选择对2^32
进行取模运算是为了保证哈希环的均匀性、范围的大小、计算效率以及可扩展性。这样可以提高一致性哈希算法的分布均衡性和系统的性能。
一致性哈希算法要进行两步哈希:
- 对存储节点进行哈希计算,即对存储节点做哈希映射,比如根据节点的 IP 地址进行哈希;
- 当需要对数据进行存储或访问时,对数据进行哈希映射
所以,一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上。
那么,对「数据」进行哈希映射得到一个结果,要怎样找到存储该数据的节点呢?答案是:映射的结果值往顺时针的方向找到的第一个节点,就是存储该数据的节点。
如上图所示,有 3 个节点经过哈希计算,映射到了哈希环上不同的位置。接着,对要查询的 key-1 进行哈希计算,确定 key-1 映射在哈希环上的位置后,从这个位置往顺时针的方向找到的第一个节点,就是存储该 key-1 数据的节点。
由于哈希环是一个闭合的环形结构,增加或删除一个服务器节点,只会影响到环上和该节点相邻的部分,而不会影响整个环。因此,当服务器节点发生变化时,只需要重新映射和迁移少量的数据即可,而不会对整个系统产生重大影响。
引入虚拟节点
但是一致性哈希算法并不能保证节点都能够在哈希环上分布均匀,这样就会带来一个问题,会有大量的请求都集中在一个节点上。在这种节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,发生雪崩式的连锁反应。
比如,下图中如果节点 A 被移除了,当节点 A 宕机后,根据一致性哈希算法的规则,其上的数据应该全部迁移到相邻的节点 B 上,这样节点 B 的数据量、访问量都会迅速增加,而一旦新增的压力超过了节点 B 的处理能力上限,就会导致节点 B 的崩溃,进而形成雪崩式的连锁反应。
所以,一致性哈希算法虽然减少了数据迁移量,但是会存在节点分布不均匀的问题,这时我们可以引入虚拟节点的概念。
要想解决节点在哈希环上分布不均匀的问题,就是需要有大量的节点,节点数越多,哈希环上的节点分布地就越均匀。具体做法是:不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到真实节点,即这里有两层映射关系。
可以看到,随着节点数量的增多,节点在哈希环上的分布就相对均匀了。这时,如果有请求寻址到 A-01 这个虚拟节点,接着再通过 A-01 虚拟节点找到其真实节点 A,这样请求就能打到真实节点 A 上了。
上图中每个真实节点仅包含 3 个虚拟节点,实际上这样能起到的均衡效果很有限。在实际的工程中,虚拟节点的数量会有很多,比如 Nginx 的一致性哈希算法,每个权重为 1 的真实节点就包含了 160 个虚拟节点。
另外,虚拟节点除了会提高节点的均衡度之外,还会提高系统的稳定性。当节点发生变化时,会有不同的节点来共同分担系统的变化,因此稳定性会更高。比如,当某个节点被移除时,对应的该节点的多个虚拟节点也会被移除,而这些虚拟节点按顺时针方向找到的下一个虚拟节点,可能会对应着不同的真实节点,即这些不同的真实节点可以共同分担因节点变化而产生的压力。而且,有了虚拟节点之后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟节点。
因此,引入虚拟节点的一致性哈希算法不仅适合于硬件配置不同的节点的场景,而且适合于节点规模会发生变化的场景。
参考资料
- https://xiaolincoding.com/os/8_network_system/hash.html#_9-4-%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C