我又发现了一个BUG
前面我介绍了Dubbo 2.6.5版本之前,最小活跃数算法的两个 bug。
很不幸,这次我又发现了Dubbo 2.7.4.1版本,一致性哈希负载均衡策略的一个bug,我提交了issue 地址如下:
https://github.com/apache/dubbo/issues/5429
我在这里详细说一下这个Bug现象、原因和我的解决方案。
现象如下,我们调用三次服务端:
输出日志如下(有部分删减):
可以看到,在三次调用的过程中并没有发生服务的上下线操作,但是每一次调用都重新进行了哈希环的映射。而我们预期的结果是应该只有在第一次调用的时候进行哈希环的映射,如果没有服务上下线的操作,后续请求根据已经映射好的哈希环进行处理。
上面输出的原因是由于每次调用的invokers的identityHashCode发生了变化:
我们看一下三次调用invokers的情况:
经过debug我们可以看出因为每次调用的invokers地址值不是同一个,所以System.identityHashCode(invokers)方法返回的值都不一样。
接下来的问题就是为什么每次调用的invokers地址值都不一样呢?
经过Debug之后,可以找到这个地方:
org.apache.dubbo.rpc.cluster.RouterChain#route
问题就出在这个TagRouter中:
org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker
所以,在TagRouter中的stream操作,改变了invokers,导致每次调用时其
System.identityHashCode(invokers)返回的值不一样。所以每次调用都会进行哈希环的映射操作,在服务节点多,虚拟节点多的情况下会有一定的性能问题。
到这一步,问题又发生了变化。这个TagRouter怎么来的呢?
如果了解Dubbo 2.7.x版本新特性的朋友可能知道,标签路由是Dubbo2.7引入的新功能。
通过加载下面的配置加载了RouterFactrory:
META-INF\dubbo\internal\org.apache.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0版本之前)
META-INF\dubbo\internal\com.alibaba.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0之前)
下面是Dubbo 2.6.7(2.6.x的最后一个版本)和Dubbo 2.7.0版本该文件的对比:
可以看到确实是在 Dubbo 2.7.0 之后引入了 TagRouter。
至此,Dubbo 2.7.0 版本之后,一致性哈希负载均衡算法的 Bug 的来龙去脉也介绍清楚了。
解决方案是什么呢?特别简单,把获取 identityHashCode 的方法从 System.identityHashCode(invokers) 修改为 invokers.hashCode() 即可。
此方案是我提的 issue 里面的评论,这里 System.identityHashCode 和 hashCode 之间的联系和区别就不进行展开讲述了,不清楚的大家可以自行了解一下。
(我的另外一篇文章:够强!一行代码就修复了我提的Dubbo的Bug。)
改完之后,我们再看看运行效果:
可以看到第二次调用的时候并没有进行哈希环的映射操作,而是直接取到了值,进行调用。
加入节点,画图分析
最后,我再分析一种情况。在A、B、C三个服务器(20881、20882、20883端口)都在正常运行,哈希映射已经完成的情况下,我们再启动一个D节点(20884端口),这时的日志输出和对应的哈希环变化情况如下:
根据日志作图如下:
根据输出日志和上图再加上源码,你再细细回味一下。我个人觉得还是讲的非常详细了。
一致性哈希的应用场景
当大家谈到一致性哈希算法的时候,首先的第一印象应该是在缓存场景下的使用,因为在一个优秀的哈希算法加持下,其上下线节点对整体数据的影响(迁移)都是比较友好的。
但是想一下为什么 Dubbo 在负载均衡策略里面提供了基于一致性哈希的负载均衡策略?它的实际使用场景是什么?
我最开始也想不明白。我想的是在 Dubbo 的场景下,假设需求是想要一个用户的请求一直让一台服务器处理,那我们可以采用一致性哈希负载均衡策略,把用户号进行哈希计算,可以实现这样的需求。但是这样的需求未免有点太牵强了,适用场景略小。
直到有天晚上,我睡觉之前,电光火石之间突然想到了一个稍微适用的场景了。
如果需求是需要保证某一类请求必须顺序处理呢?
如果你用其他负载均衡策略,请求分发到了不同的机器上去,就很难保证请求的顺序处理了。比如A,B请求要求顺序处理,现在A请求先发送,被负载到了A服务器上,B请求后发送,被负载到了B服务器上。而B服务器由于性能好或者当前没有其他请求或者其他原因极有可能在A服务器还在处理A请求之前就把B请求处理完成了。这样不符合我们的要求。
这时,一致性哈希负载均衡策略就上场了,它帮我们保证了某一类请求都发送到固定的机器上去执行。比如把同一个用户的请求发送到同一台机器上去执行,就意味着把某一类请求发送到同一台机器上去执行。所以我们只需要在该机器上运行的程序中保证顺序执行就行了,比如你加一个队列。
一致性哈希算法+队列,可以实现顺序处理的需求。
好了,一致性哈希负载均衡算法就写到这里。
继续进入下一个议题。
加权轮询负载均衡
这一小节是对于Dubbo负载均衡策略之一的加权随机算法的详细分析。
从 2.6.4 版本聊起,该版本在某些情况下存在着比较严重的性能问题。由问题入手,层层深入,了解该算法在 Dubbo 中的演变过程,读懂它的前世今生。
什么是轮询?
在描述加权轮询之前,先解释一下什么是轮询算法,如下图所示:
假设我们有A、B、C三台服务器,共计处理6个请求,服务处理请求的情况如下:
- 第一个请求发送给了A服务器
- 第二个请求发送给了B服务器
- 第三个请求发送给了C服务器
- 第四个请求发送给了A服务器
- 第五个请求发送给了B服务器
- 第六个请求发送给了C服务器
- ......
上面这个例子演示的过程就叫做轮询。可以看出,所谓轮询就是将请求轮流分配给每台服务器。
轮询的优点是无需记录当前所有服务器的链接状态,所以它一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。
轮询的缺点也是显而易见的,它的应用场景要求所有服务器的性能都相同,非常的局限。
大多数实际情况下,服务器性能是各有差异,针对性能好的服务器,我们需要让它承担更多的请求,即需要给它配上更高的权重。
所以加权轮询,应运而生。
什么是加权轮询?
为了解决轮询算法应用场景的局限性。当遇到每台服务器的性能不一致的情况,我们需要对轮询过程进行加权,以调控每台服务器的负载。
经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:3:2。那么在10次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的3次请求,服务器 C 则收到其中的2次请求。
这里要和加权随机算法做区分哦。直接把前面介绍的加权随机算法画的图拿过来:
上面这图是按照比例画的,可以直观的看到,对于某一个请求,区间(权重)越大的服务器,就越可能会承担这个请求。所以,当请求足够多的时候,各个服务器承担的请求数,应该就是区间,即权重的比值。
假设有A、B、C三台服务器,权重之比为5:3:2,一共处理10个请求。
那么负载均衡采用加权随机算法时,很有可能A、B服务就处理完了这10个请求,因为它是随机调用。
采用负载均衡采用轮询加权算法时,A、B、C服务一定是分别承担5、3、2个请求。
Dubbo2.6.4版本的实现
对于Dubbo2.6.4版本的实现分析,可以看下图,我加了很多注释,其中的输出语句都是我加的:
示例代码还是沿用之前文章中的Demo,这里分别在 20881、20882、20883 端口启动三个服务,各自的权重分别为 1,2,3。
客户端调用 8 次:
输出结果如下:
可以看到第七次调用后mod=0,回到了第一次调用的状态。形成了一个闭环。
再看看判断的条件是什么:
其中mod在代码中扮演了极其重要的角色,mod根据一个方法的调用次数不同而不同,取值范围是[0,weightSum)。
因为weightSum=6,所以列举mod不同值时,最终的选择结果和权重变化:
可以看到20881,20882,20883承担的请求数量比值为1:2:3。同时我们可以看出,当 mod >= 1 后,20881端口的服务就不会被选中了,因为它的权重被减为0了。当 mod >= 4 后,20882端口的服务就不会被选中了,因为它的权重被减为0了。
结合判断条件和输出结果,我们详细分析一下(下面内容稍微有点绕,如果看不懂,多结合上面的图片看几次):