一致性哈希负载均衡
这一部分是对于Dubbo负载均衡策略之一的一致性哈希负载均衡的详细分析。对源码逐行解读、根据实际运行结果,配以丰富的图片,可能是东半球讲一致性哈希算法在Dubbo中的实现最详细的文章了。
本小节所示源码,没有特别标注的地方,均为2.7.4.1版本。
在撰写本文的过程中,发现了Dubbo2.7.0版本之后的一个bug。会导致性能问题,如果你们的负载均衡配置的是一致性哈希或者考虑使用一致性哈希的话,可以了解一下。
哈希算法
在介绍一致性哈希算法之前,我们看看哈希算法,以及它解决了什么问题,带来了什么问题。
如上图所示,假设0,1,2号服务器都存储的有用户信息,那么当我们需要获取某用户信息时,因为我们不知道该用户信息存放在哪一台服务器中,所以需要分别查询0,1,2号服务器。这样获取数据的效率是极低的。
对于这样的场景,我们可以引入哈希算法。
是上面的场景,但前提是每一台服务器存放用户信息时是根据某一种哈希算法存放的。所以取用户信息的时候,也按照同样的哈希算法取即可。
假设我们要查询用户号为100的用户信息,经过某个哈希算法,比如这里的userId mod n,即100 mod 3结果为1。所以用户号100的这个请求最终会被1号服务器接收并处理。
这样就解决了无效查询的问题。
但是这样的方案会带来什么问题呢?
扩容或者缩容时,会导致大量的数据迁移。最少也会影响百分之50的数据。
为了说明问题,我们加入一台服务器3。服务器的数量n就从3变成了4。还是查询用户号为100的用户信息时,100 mod 4结果为0。这时,请求就被0号服务器接收了。
当服务器数量为3时,用户号为100的请求会被1号服务器处理。
当服务器数量为4时,用户号为100的请求会被0号服务器处理。
所以,当服务器数量增加或者减少时,一定会涉及到大量数据迁移的问题。可谓是牵一发而动全身。
对于上诉哈希算法其优点是简单易用,大多数分库分表规则就采取的这种方式。一般是提前根据数据量,预先估算好分区数。
其缺点是由于扩容或收缩节点导致节点数量变化时,节点的映射关系需要重新计算,会导致数据进行迁移。所以扩容时通常采用翻倍扩容,避免数据映射全部被打乱,导致全量迁移的情况,这样只会发生50%的数据迁移。
假设这是一个缓存服务,数据的迁移会导致在迁移的时间段内,有缓存是失效的。
缓存失效,可怕啊。还记得我之前的文章吗,《当周杰伦把QQ音乐干翻的时候,作为程序猿我看到了什么?》就是讲缓存击穿、缓存穿透、缓存雪崩的场景和对应的解决方案。
一致性哈希算法
为了解决哈希算法带来的数据迁移问题,一致性哈希算法应运而生。
对于一致性哈希算法,官方说法如下:
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题。
什么意思呢?我用大白话加画图的方式给你简单的介绍一下。
一致性哈希,你可以想象成一个哈希环,它由0到2^32-1个点组成。A,B,C分别是三台服务器,每一台的IP加端口经过哈希计算后的值,在哈希环上对应如下:
当请求到来时,对请求中的某些参数进行哈希计算后,也会得出一个哈希值,此值在哈希环上也会有对应的位置,这个请求会沿着顺时针的方向,寻找最近的服务器来处理它,如下图所示:
一致性哈希就是这么个东西。那它是怎么解决服务器的扩容或收缩导致大量的数据迁移的呢?
看一下当我们使用一致性哈希算法时,加入服务器会发什么事情。
当我们加入一个D服务器后,假设其IP加端口,经过哈希计算后落在了哈希环上图中所示的位置。
这时影响的范围只有图中标注了五角星的区间。这个区间的请求从原来的由C服务器处理变成了由D服务器请求。而D到C,C到A,A到B这个区间的请求没有影响,加入D节点后,A、B服务器是无感知的。
所以,在一致性哈希算法中,如果增加一台服
务器,则受影响的区间仅仅是新服务器(D)在哈希环空间中,逆时针方向遇到的第一台服务器(B)之间的区间,其它区间(D到C,C到A,A到B)不会受到影响。
在加入了D服务器的情况下,我们再假设一段时间后,C服务器宕机了:
C服务器宕机后,影响的范围也是图中标注了五角星的区间。C节点宕机后,B、D服务器是无感知的。
所以,在一致性哈希算法中,如果宕机一台服务器,则受影响的区间仅仅是宕机服务器(C)在哈希环空间中,逆时针方向遇到的第一台服务器(D)之间的区间,其它区间(C到A,A到B,B到D)不会受到影响。
综上所述,在一致性哈希算法中,不管是增加节点,还是宕机节点,受影响的区间仅仅是增加或者宕机服务器在哈希环空间中,逆时针方向遇到的第一台服务器之间的区间,其它区间不会受到影响。
是不是很完美?
不是的,理想和现实的差距是巨大的。
一致性哈希算法带来了什么问题?
当节点很少的时候可能会出现这样的分布情况,A服务会承担大部分请求。这种情况就叫做数据倾斜。
怎么解决数据倾斜呢?加入虚拟节点。
怎么去理解这个虚拟节点呢?
首先一个服务器根据需要可以有多个虚拟节点。假设一台服务器有n个虚拟节点。那么哈希计算时,可以使用IP+端口+编号的形式进行哈希值计算。其中的编号就是0到n的数字。由于IP+端口是一样的,所以这n个节点都是指向的同一台机器。
如下图所示:
在没有加入虚拟节点之前,A服务器承担了绝大多数的请求。但是假设每个服务器有一个虚拟节点(A-1,B-1,C-1),经过哈希计算后落在了如上图所示的位置。那么A服务器的承担的请求就在一定程度上(图中标注了五角星的部分)分摊给了B-1、C-1虚拟节点,实际上就是分摊给了B、C服务器。
一致性哈希算法中,加入虚拟节点,可以解决数据倾斜问题。
当你在面试的过程中,如果听到了类似于数据倾斜的字眼。那大概率是在问你一致性哈希算法和虚拟节点。
在介绍了相关背景后,我们可以去看看一致性哈希算法在Dubbo中的应用了。
一致性哈希算法在Dubbo中的应用
前面我们说了Dubbo中负载均衡的实现是通过
org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance中的 doSelect 抽象方法实现的,一致性哈希负载均衡的实现类如下所示:
org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
由于一致性哈希实现类看起来稍微有点抽象,不太好演示,所以我想到了一个"骚"操作。前面的文章说过 LoadBalance 是一个 SPI 接口:
既然是一个 SPI 接口,那我们可以自己扩展一个一模一样的算法,只是在算法里面加入一点输出语句方便我们观察情况。怎么扩展 SPI 接口就不描述了,只要记住代码里面的输出语句都是额外加的,此外没有任何改动即可,如下:
整个类如下图片所示,请先看完整个类,有一个整体的概念后,我会进行方法级别的分析。
图片很长,其中我加了很多注释和输出语句,可以点开大图查看,一定会帮你更加好的理解一致性哈希在Dubbo中的应用:
改造之后,我们先把程序跑起来,有了输出就好分析了。
服务端代码如下:
其中的端口是需要手动修改的,我分别启动服务在20881和20882端口。
项目中provider.xml配置如下:
consumer.xml配置如下:
然后,启动在20881和20882端口分别启动两个服务端。客户端消费如下:
运行结果输出如下,可以先看个大概的输出,下面会对每一部分输出进行逐一的解读。
好了,用例也跑起来了,日志也有了。接下来开始结合代码和日志进行方法级别的分析。
首先是doSelect方法的入口:
从上图我们知道了,第一次调用需要对selectors进行put操作,selectors的 key 是接口中定义的方法,value 是 ConsistentHashSelector 内部类。
ConsistentHashSelector通过调用其构造函数进行初始化的。invokers(服务端)作为参数传递到了构造函数中,构造函数里面的逻辑,就是把服务端映射到哈希环上的过程,请看下图,结合代码,仔细分析输出数据:
从上图可以看出,当 ConsistentHashSelector 的构造方法调用完成后,8个虚拟节点在哈希环上已经映射完成。两台服务器,每一台4个虚拟节点组成了这8个虚拟节点。
doSelect方法继续执行,并打印出每个虚拟节点的哈希值和对应的服务端,请仔细品读下图:
说明一下:上面图中的哈希环是没有考虑比例的,仅仅是展现了两个服务器在哈希环上的相对位置。而且为了演示说明方便,仅仅只有8个节点。假设我们有4台服务器,每台服务器的虚拟节点是默认值(160),这个情况下哈希环上一共有160*4=640个节点。
哈希环映射完成后,接下来的逻辑是把这次请求经过哈希计算后,映射到哈希环上,并顺时针方向寻找遇到的第一个节点,让该节点处理该请求:
还记得地址为 468e8565 的 A 服务器是什么端口吗?前面的图片中有哦,该服务对应的端口是 20882 。
最后我们看看输出结果:
和我们预期的一致。整个调用就算是完成了。
再对两个方法进行一个补充说明。
第一个方法是 selectForKey,这个方法里面逻辑如下图所示:
虚拟节点都存储在 TreeMap 中。顺时针查询的逻辑由 TreeMap 保证。看一下下面的 Demo 你就明白了。
第二个方法是 hash 方法,其中的 & 0xFFFFFFFFL 的目的如下:
&是位运算符,而 0xFFFFFFFFL 转换为四字节表现后,其低32位全是1,所以保证了哈希环的范围是 [0,Integer.MAX_VALUE]:
所以这里我们可以改造这个哈希环的范围,假设我们改为 100000。十进制的 100000 对于的 16 进制为 186A0 。所以我们改造后的哈希算法为:
再次调用后可以看到,计算后的哈希值都在10万以内。但是分布极不均匀,说明修改数据后这个哈希算法不是一个优秀的哈希算法:
以上,就是对一致性哈希算法在Dubbo中的实现的解读。需要特殊说明一下的是,一致性哈希负载均衡策略和权重没有任何关系。