基于一致性哈希实现负载均衡
垂直扩展VS.水平扩展
在单体架构中,客户端通常向单个服务器发出请求。随着请求的数量增加,单个服务器没有足够的容量来处理所有传入的请求。
垂直扩展是一种选择,即向服务器添加更多的CPU/内存。该选择只能工作在没达到硬件的极限之前。
在大多数情况下,水平扩展(添加更多服务器)通常是一种更具可伸缩性的可选方案。
垂直扩展VS.水平扩展
使用负载均衡器重定向请求
当水平扩展时,请求被发送到负载均衡器,而不是直接发送到服务器。负载均衡器的用途,顾名思义:它的目的是通过尽可能均衡地分发请求来平衡每个服务器上的负载。
哈希函数和取模
可视化映射过程
让我们回顾一下如何使用数组作为数据结构将每个请求映射到服务器。
在下面的这个简单示例中,数组的索引直接映射到服务器ID,但在生产中不一定如此。因此,使用数组这种数据结构可以让我们更灵活地将输出映射到我们想要的任何服务器。
使用数组索引作为服务器ID
如果添加服务器会怎么样?
到目前为止,我们假设服务器的数量是固定的。但是,由于我们选择了水平扩展,我们应该能够根据需要添加或删除服务器。不幸的是,简单地使用哈希函数和取模会影响其他请求的处理和重定向结果。让我们通过下面的例子来了解增删服务器产生的影响。
但是,如果我们添加一个额外的服务器,我们将得到一个值(88 % 6),这将导致请求重定向到服务器4。
请求发生改变的影响
这种重定向可能看起来微不足道,但当服务器是有状态的情况,就会产生多余的开销。尽管HTTP是一种无状态协议,但一些服务器为了优化可能会在其缓存中存储一些与用户相关的数据。例如,服务器可以选择存储会话日志来记住用户,从而减少身份验证的频率。
更改的成本是很高的,特别是在处理成千上万的服务器时。那么,在添加或删除服务器时,如何减少对其他服务器的影响呢?
一致性哈希
解决方案是使用一致性哈希。让我们首先尝试用三个步骤来形象化这个概念。
第一步:将请求映射到一个整数环上
现在,让我们想象一个循环数组,而不是常规数组。与数组类似,每个请求现在都映射到哈希环上的一个位置。
每个请求都会映射到哈希环上的一个值
第二步:将服务器哈希处理映射到环
服务器ID映射到哈希环上
顺时针移动
现在我们已经将请求和服务器映射到一个环上,最后一步很简单。对每个请求,我们只是按顺时针方式找到其右侧最近的服务器。例如,映射到索引7的请求由映射到索引9的服务器提供服务。
一致哈希是如何最小化对其他服务器的影响的?
由于请求被转发到最右边的服务器进行处理,所以增加服务器,最多只有另一台服务器会受到服务器数量变化的影响。
在上面的示例中,添加了一个新服务器,它映射到索引95。此时映射到索引88的请求会映射到索引95新服务器,而像之前那样映射到索引99的服务器。
在这种情况下,只有映射到索引99的服务器需要使其缓存失效。类似地,如果一个服务器被删除,服务器的下一个邻居将接管被删除服务器的所有请求,而其他服务器将不受影响。
在寻找最近的邻居时,一致性哈希的概念避免了强加给其他服务器的昂贵更改成本,并将成本降低到一个常数。
虚拟节点
请求不是完全随机情况
在理想情况下,请求是均匀随机的,每个服务器处理的负载量是均匀的。
然而,这在现实中很少发生。例如,某个特定区域可能有更多的请求,这意味着与其他服务器相比,某个服务器的负载更高。
为了负载均衡,我们需要在哈希环中请求映射密集的范围放置更多的服务器。给定数量的服务器,能做到这一点吗?
使用多个哈希函数
答案是肯定的。回想一下,每个哈希函数都是不同的,并且返回不同的输出。如果我们取服务器ID并使用三个不同的哈希函数对其进行散列,我们将得到三个不同的输出。如果我们把这三个不同的值映射到哈希环上,它们就会在不同的位置。
这种在服务器ID上使用多个哈希函数的方法,会在哈希环上创建虚拟位置,或者我们称之为虚拟节点。因此,我们在环上会得到一个服务器位置分布更均衡的结果,这可以帮助减少每个服务器的负载。