一、如何分配请求?
大多数网站背后肯定不是只有一台服务器提供服务,因为单机的并发量和数据量都是有限的,所以都会用多台服务器构成集群来对外提供服务。
那么现在有那么多个节点(服务器节点),要如何分配客户端的请求呢?
其实这个问题就是“负载均衡问题”。解决负载均衡问题的算法很多,不同的负载均衡算法,对应的就是不同的分配策略,适应的业务场景也不同。
最简单的方式,引入一个中间的负载均衡层,让它将外界的请求轮流的转发给内部的集群。比如集群有3个节点,外界请求有3个,那么每个节点都会处理1个请求,达到了分配请求的目的。
考虑到每个节点的硬件配置有所区别,我们可以引入权重值,将硬件配置更好的节点的权重值设高,然后根据各个节点的权重值,按照一定比重分配在不同的节点上,让硬件配置更好的节点承担更多的请求,这种算法叫做加权轮询。
加权轮询算法使用场景是建立在每个节点存储的数据都是相同的前提。所以,每次读数据的请求,访问任意一个节点都能得到结果。
但是,加权轮询算法是无法应对分布式系统(数据分片的系统)的,因为分布式系统中,每个节点存储的数据是不同的。
当我们想要提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。比如一个分布式KV(key-value)缓存系统,某个key应该到哪个或者哪些节点上获得应该是确定的,不是说任意访问一个节点都可以得到缓存结果的。
二、使用哈希算法有什么问题?
因为对同一个关键字进行哈希计算,每次计算都是相同的值,这样就可以将某个key确定到一个节点了,可以满足分布式系统的负载均衡需求。
哈希算法最简单的做法就是进行取模运算,比如分布式系统中有3个节点,基于hash(key)%3公式对数据进行了映射。
如果客户端要获取指定key的数据,通过下面的公式可以定位节点。
hash(key)%3
如果经过上面这个公式计算后得到的值是0,就说明该key需要去第一个节点获取。
但是对于哈希算法,如果节点数量发生了变化,也就是在对系统做扩容或者缩容时,必须迁移改变了映射关系的数据,否则会出现查询不到数据的问题。(哈希失效)
同样的道理,如果我们对分布式系统进行缩容,比如移除一个节点,也会因为取模哈希函数中基数的变化,可能出现查询不到数据的问题。
要解决这个问题的办法,就需要我们进行数据迁移,比如节点的数量从3变化为4时,要基于新的计算公式hash(key)%4,重新对数据和节点做映射(重新对各个节点进行哈希,原本的哈希可能大规模失效)。哈希算法在面对节点数量变化时,最坏情况下所有数据都需要迁移,所以它的数据迁移规模是O(n),这样数据的迁移成本太高了。
所以,我们应该要重新想一个新的算法,来避免分布式系统在扩容或者缩容时,发生过多的数据迁移。
三、使用一致性哈希算法有什么问题?
一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题。
一致性哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致性哈希算法是对2^32进行取模运算,是一个固定的值。
我们可以把一致性哈希算法是对2^32进行取模运算的结果值组织成一个圆环,这个圆环被称为哈希环。
一致性哈希要进行两步哈希:
第一步:对存储节点进行哈希计算,也就是对存储节点做哈希映射,比如根据节点的IP地址进行哈希;
第二步:当对数据进行存储或访问时,对数据进行哈希映射。
所以,一致性哈希是指将存储节点和数据都映射到一个首尾相连的哈希环上。
那么,对数据进行哈希映射得到一个结果要怎么找到存储该数据的节点呢?
答案是,映射的结果值往顺时针的方向找到第一个节点,就是存储该数据的节点。
举个例子,有3个节点经过哈希计算,映射到了如下图的位置:
接着,对要查询的key-01进行哈希计算,确定此key-01映射在哈希环的位置,然后从这个位置往顺时针的方向找到第一个节点,就是存储该key-01数据的节点。
比如,下图中的key-01映射的位置,往顺时针的方向找到第一个节点就是节点A。
所以,当需要对指定key的值进行读写的时候,要通过下面2步进行寻址:
首先,对key进行哈希计算,确定此key在环上的位置;
然后,从这个位置沿着顺时针方向走,遇到的第一个节点就是存储key的节点。
知道了一致性哈希寻址的方式,来看看,如果增加一个节点或者减少一个节点会发生大量的数据迁移吗?
假设节点数量从3增加到了4,新的节点D经过哈希计算后映射到了下图中的位置。
你可以看到,key-01,key-03都不受影响,只有key-02需要被迁移节点D。
假设节点数量从3减少到了2,比如将节点A移除:
可以看到,key-02和key-03不会受到影响,只有key-01需要被迁移节点B。
因此,在一致性哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其他数据也不会受到影响。
上面这些图中3个节点映射在哈希环还是比较分散的,所以看起来请求都会均衡到每个节点。
但是一致性哈希算法并不保证节点能够在哈希环上分布均匀,这样会带来一个问题,会有大量的请求集中在一个节点上。
这时候就一点都不负载均衡了。
另外,在这种节点分布不均匀的情况下,进行扩容或缩容时,哈希环上的相邻节点容易受到过大影响,容易发生雪崩式的连锁反应。
比如,上图中如果节点A被移除了,当节点A宕机后,根据一致性哈希算法的规则,其上数据应该全部迁移到相邻的节点B上,这样,节点B的数据量、访问量都会迅速增加很多倍,一旦新增的压力超过了节点B的处理能力上限,就会导致节点B崩溃,进而形成雪崩式的连锁反应。
所以,一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题。
四、如何通过虚拟节点提高均衡度?
要想解决节点能在哈希环上分配不均匀的问题,就要有大量的节点,节点数越多,哈希环上的节点分布的就越均匀。
但问题是,实际中我们没有那么多节点。所以这个时候我们就加入虚拟节点,也就是对一个真实/物理节点做多个副本。
具体做法是,不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有两层映射关系。
引入,虚拟节点后,原本哈希环上只有3个节点的情况,就会变成有9个虚拟节点映射到哈希环上,哈希环上的节点数量多了3倍。
你可以看到,节点数量多了后,节点在哈希环上的分布就相对均匀了。这时候,如果有访问请求寻址到A-01这个虚拟节点,接着再通过A-01虚拟节点找到真实节点A,这样请求就能访问到真实节点A了。
在实际工程中,虚拟节点数量会大很多,比如Nginx的一致性哈希算法,每个权重为1的真实节点就含有160个虚拟节点。
另外,虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高。
比如,当某个节点被移除时,对应该节点的多个虚拟节点均会被移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,坑会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。
而且,有了虚拟节点后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟节点即可。
因此,带虚拟节点的一致性哈希算法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景。