Dubbo的五种负载均衡策略
2020 年 5 月 15 日,Dubbo 发布 2.7.7 release 版本。其中有这么一个 Features
新增一个负载均衡策略。
熟悉我的老读者肯定是知道的,Dubbo 的负载均衡我都写过专门的文章,对每个负载均衡算法进行了源码的解读,还分享了自己调试过程中的一些骚操作。
新的负载均衡出来了,那必须的得解读一波。
先看一下提交记录:
https://github.com/chickenlj/incubator-dubbo/commit/6d2ba7ec7b5a1cb7971143d4262d0a1bfc826d45
负载均衡是基于 SPI 实现的,我们看到对应的文件中多了一个名为 shortestresponse 的 key。
这个,就是新增的负载均衡策略了。看名字,你也知道了这个策略的名称就叫:最短响应。
所以截止 2.7.7 版本,官方提供了五种负载均衡算法了,他们分别是:
-
ConsistentHashLoadBalance 一致性哈希负载均衡
-
LeastActiveLoadBalance 最小活跃数负载均衡
-
RandomLoadBalance 加权随机负载均衡
-
RoundRobinLoadBalance 加权轮询负载均衡
-
ShortestResponseLoadBalance 最短响应时间负载均衡
前面四种我已经在之前的文章中进行了详细的分析。有的读者反馈说想看合辑,所以我会在这篇文章中把之前文章也整合进来。
所以,需要特别强调一下的是,这篇文章集合了之前写的三篇负载均衡的文章。看完最短响应时间负载均衡这一部分后,如果你看过我之前的那三篇文章,你可以温故而知新,也可以直接拉到文末看看我推荐的一个活动,然后点个赞再走。如果你没有看过那三篇,这篇文章如果你细看,肯定有很多收获,以后谈起负载均衡的时候若数家珍,但是肯定需要看非常非常长的时间,做好心理准备。
我已经预感到了,这篇文章妥妥的会超过 2 万字。属于硬核劝退文章,想想就害怕。
最短响应时间负载均衡
首先,我们看一下这个类上的注解,先有个整体的认知。
org.apache.dubbo.rpc.cluster.loadbalance.ShortestResponseLoadBalance
我来翻译一下是什么意思:
-
从多个服务提供者中选择出调用成功的且响应时间最短的服务提供者,由于满足这样条件的服务提供者有可能有多个。所以当选择出多个服务提供者后要根据他们的权重做分析。
-
但是如果只选择出来了一个,直接用选出来这个。
-
如果真的有多个,看它们的权重是否一样,如果不一样,则走加权随机算法的逻辑。
-
如果它们的权重是一样的,则随机调用一个。
再配个图,就好理解了,可以先不管图片中的标号:
有了上面的整体概念的铺垫了,接下来分析源码的时候就简单了。
源码一共就 66 行,我把它分为 5 个片段去一一分析。
这里一到五的标号,对应上面流程图中的标号。我们一个个的说。
标号为①的部分
这一部分是定义并初始化一些参数,为接下来的代码服务的,翻译一下每个参数对应的注释:
-
length 参数:服务提供者的数量。
-
shortestResponse 参数:所有服务提供者的估计最短响应时间。(这个地方我觉得注释描述的不太准确,看后面的代码可以知道这只是一个零时变量,在循环中存储当前最短响应时间是多少。)
-
shortCount 参数:具有相同最短响应时间的服务提供者个数,初始化为 0。
-
shortestIndexes 参数:数组里面放的是具有相同最短响应时间的服务提供者的下标。
-
weights 参数:每一个服务提供者的权重。
-
totalWeight 参数:多个具有相同最短响应时间的服务提供者对应的预热(预热这个点还是挺重要的,在下面讲最小活跃数负载均衡的时候有详细说明)权重之和。
-
firstWeight 参数:第一个具有最短响应时间的服务提供者的权重。
-
sameWeight 参数:多个满足条件的提供者的权重是否一致。
标号为②的部分
这一部分代码的关键,就在上面框起来的部分。而框起来的部分,最关键的地方,就在于第一行。
获取调用成功的平均时间。
成功调用的平均时间怎么算的?
调用成功的请求数总数对应的总耗时 / 调用成功的请求数总数 = 成功调用的平均时间。
所以,在下面这个方法中,首先获取到了调用成功的请求数总数:
这个 succeeded 参数是怎么来的呢?
答案就是:总的请求数减去请求失败的数量,就是请求成功的总数!
那么为什么不能直接获取请求成功的总数呢?
别问,问就是没有这个选项啊。你看,在 RpcStatus 里面没有这个参数呀。
请求成功的总数我们有了,接下来成功总耗时怎么拿到的呢?
答案就是:总的请求时间减去请求失败的总时间,就是请求成功的总耗时!
那么为什么不能直接获取请求成功的总耗时呢?
别问,问就是......
我们看一下 RpcStatus 中的这几个参数是在哪里维护的:
org.apache.dubbo.rpc.RpcStatus#endCount(org.apache.dubbo.rpc.RpcStatus, long, boolean)
其中的第二个入参是本次请求调用时长,第三个入参是本次调用是否成功。
具体的方法不必细说了吧,已经显而易见了。
再回去看框起来的那三行代码:
-
第一行获取到了该服务提供者成功请求的平均耗时。
-
第二行获取的是该服务提供者的活跃数,也就是堆积的请求数。
-
第三行获取的就是如果当前这个请求发给这个服务提供者预计需要等待的时间。乘以 active 的原因是因为它需要排在堆积的请求的后面嘛。
这里,我们就获取到了如果选择当前循环中的服务提供者的预计等待时间是多长。
后面的代码怎么写?
当然是出来一个更短的就把这个踢出去呀,或者出来一个一样长时间的就记录一下,接着去 pk 权重了。
所以,接下来 shortestIndexes 参数和 weights 参数就排上用场了:
另外,多说一句的,它里面有这样的一行注释:
和 LeastActiveLoadBalance 负载均衡策略一致,我给你截图对比一下:
可以看到,确实是非常的相似,只是一个是判断谁的响应时间短,一个是判断谁的活跃数低。
标号为③的地方
标号为③的地方是这样的:
里面参数的含义我们都知道了,所以,标号为③的地方的含义就很好解释了:经过选择后只有一个服务提供者满足条件。所以,直接使用这个服务提供者。
标号为④的地方
这个地方我就不展开讲了(后面的加权随机负载均衡那一小节有详细说明),熟悉的朋友一眼就看出来这是加权随机负载均衡的写法了。
不信?我给你对比一下:
你看,是不是一模一样的。
标号为⑤的地方
一行代码,没啥说的。就是从多个满足条件的且权重一样的服务提供者中随机选择一个。
如果一定要多说一句的话,我截个图吧:
可以看到,这行代码在最短响应时间、加权随机、最小活跃数负载均衡策略中都出现了,且都在最后一行。
好了,到这里最短响应时间负载均衡策略就讲完了,你再回过头去看那张流程图,会发现其实流程非常的清晰,完全可以根据代码结构画出流程图。一个是说明这个算法是真的不复杂,另一个是说明好的代码会说话。
优雅
你知道 Dubbo 加入这个新的负载均衡算法提交了几个文件吗?
四个文件,其中还包含两个测试文件:
这里就是策略模式和 SPI 的好处。对原有的负载均衡策略没有任何侵略性。只需要按照规则扩展配置文件,实现对应接口即可。
这是什么?
这就是值得学习优雅!
那我们优雅的进入下一议题。
最小活跃数负载均衡
这一小节所示源码,没有特别标注的地方均为 2.6.0 版本。
为什么没有用截止目前(我当时写这段文章的时候是2019年12月01日)的最新的版本号 2.7.4.1 呢?因为 2.6.0 这个版本里面有两个 bug 。从 bug 讲起来,印象更加深刻。
最后会对 2.6.0/2.6.5/2.7.4.1 版本进行对比,通过对比学习,加深印象。
我这里补充一句啊,仅仅半年的时间,版本号就从 2.7.4.1 到了 2.7.7。其中还包含一个 2.7.5 这样的大版本。
所以还有人说 Dubbo 不够活跃?(几年前的文章现在还有人在发。)
对吧,我们不吵架,我们摆事实,聊数据嘛。
Demo 准备
我看源码的习惯是先搞个 Demo 把调试环境搭起来。然后带着疑问去抽丝剥茧的 Debug,不放过在这个过程中在脑海里面一闪而过的任何疑问。
这一小节分享的是Dubbo负载均衡策略之一最小活跃数(LeastActiveLoadBalance)。所以我先搭建一个 Dubbo 的项目,并启动三个 provider 供 consumer 调用。
三个 provider 的 loadbalance 均配置的是 leastactive。权重分别是默认权重、200、300。
默认权重是多少?后面看源码的时候,源码会告诉你。
三个不同的服务提供者会给调用方返回自己是什么权重的服务。
启动三个实例。(注:上面的 provider.xml 和 DemoServiceImpl 其实只有一个,每次启动的时候手动修改端口、权重即可。)
到 zookeeper 上检查一下,服务提供者是否正常:
可以看到三个服务提供者分别在 20880、20881、20882 端口。(每个红框的最后5个数字就是端口号)。
最后,我们再看服务消费者。消费者很简单,配置consumer.xml
直接调用接口并打印返回值即可。
断点打在哪?
相信很多朋友也很想看源码,但是不知道从何处下手。处于一种在源码里面"乱逛"的状态,一圈逛下来,收获并不大。
这一部分我想分享一下我是怎么去看源码。首先我会带着问题去源码里面寻找答案,即有针对性的看源码。
如果是这种框架类的,正如上面写的,我会先翻一翻官网(Dubbo 的官方文档其实写的挺好了),然后搭建一个简单的 Demo 项目,然后 Debug 跟进去看。Debug 的时候当然需要是设置断点的,那么这个断点如何设置呢?
第一个断点,当然毋庸置疑,是打在调用方法的地方,比如本文中,第一个断点是在这个地方:
接下里怎么办?
你当然可以从第一个断点处,一步一步的跟进去。但是在这个过程中,你发现了吗?大多数情况你都是被源码牵着鼻子走的。本来你就只带着一个问题去看源码的,有可能你Debug了十分钟,还没找到关键的代码。也有可能你Debug了十分钟,问题从一个变