持续输出原创文章,这是why技术的第16篇原创文章git
本文是对于Dubbo负载均衡策略之一的最小活跃数算法的详细分析。文中所示源码,没有特别标注的地方均为2.6.0版本。github
为何没有用截止目前的最新的版本号2.7.4.1呢?由于2.6.0这个版本里面有两个bug。从bug讲起来,印象更加深入。web
最后会对2.6.0/2.6.5/2.7.4.1版本进行对比,经过对比学习,加深印象。算法
第一节:Demo准备。apache
本小节主要是为了演示方便,搭建了一个Demo服务。Demo中启动三个服务端,负载均衡策略均是最小活跃数,权重各不相同。编程
第二节:断点打在哪?服务器
本小节主要是分享我看源码的方式。以及咱们看源码时断点如何设置,怎么避免在源码里面"瞎逛"。mybatis
第三节:模拟环境。负载均衡
本小节主要是基于Demo的改造,模拟真实环境。在此过程当中发现了问题,引伸出下一小节。框架
第四节:active为何是0?
本小节主要介绍了RpcStatus类中的active字段在最小活跃数算法中所承担的做用,以及其何时发生变化。让读者明白为何须要在customer端配置ActiveLimitFilter拦截器。
第五节:剖析源码
本小节对于最小活跃数算法的实现类进行了逐行代码的解读,基本上在每一行代码上加入了注释。属于全文重点部分。
第六节:Bug在哪里?
逐行解读完源码后,引出了2.6.0版本最小活跃数算法的两个Bug。并经过2.6.0/2.6.5/2.7.4.1三个版本的异同点进行交叉对比,加深读者印象。
**第七节:意外收获 **
看官方文档的时候发现了一处小小的笔误,我对其进行了修改并被merged。主要是介绍给开源项目贡献代码的流程。
PS:前一到三节主要是分享我看源码的一点思路和技巧,若是你不感兴趣能够直接从第四节开始看起。本文的重点是第四到第六节。
另:阅读本文须要对Dubbo有必定的了解。
我看源码的习惯是先搞个Demo把调试环境搭起来。而后带着疑问去抽丝剥茧的Debug,不放过在这个过程当中在脑海里面一闪而过的任何疑问。
这篇文章分享的是Dubbo负载均衡策略之一最小活跃数(LeastActiveLoadBalance)。因此我先搭建一个Dubbo的项目,并启动三个provider供consumer调用。
三个provider的loadbalance均配置的是leastactive。权重分别是默认权重、200、300。
**默认权重是多少?**后面看源码的时候,源码会告诉你。
三个不一样的服务提供者会给调用方返回本身是什么权重的服务。
启动三个实例。(注:上面的provider.xml和DemoServiceImpl其实只有一个,每次启动的时候手动修改端口、权重便可。)
到zookeeper上检查一下,服务提供者是否正常:
能够看到三个服务提供者分别在20880、2088一、20882端口。(每一个红框的最后5个数字就是端口号)。
最后,咱们再看服务消费者。消费者很简单,配置consumer.xml
直接调用接口并打印返回值便可。
相信不少朋友也很想看源码,可是不知道从何处下手。处于一种在源码里面"乱逛"的状态,一圈逛下来,收获并不大。
这一小节我想分享一下我是怎么去看源码。首先我会带着问题去源码里面寻找答案,即有针对性的看源码。
若是是这种框架类的,正如上面写的,我会先搭建一个简单的Demo项目,而后Debug跟进去看。Debug的时候固然须要是设置断点的,那么这个断点如何设置呢?
第一个断点,毋庸置疑,是打在调用方法的地方,好比本文中,第一个断点是在这个地方:
接下来怎么办?
你固然能够从第一个断点处,一步一步的跟进去。可是在这个过程当中,你发现了吗?大多数状况你都是被源码牵着鼻子走的。原本你就只带着一个问题去看源码的,有可能你Debug了十分钟,还没找到关键的代码。也有可能你Debug了十分钟,问题从一个变成了无数个。
那么咱们怎么避免被源码牵着四处乱逛呢?咱们得找到一个突破口,还记得我在《很开心,在使用mybatis的过程当中我踩到一个坑。》这篇文章中提到的逆向排查的方法吗?此次的文章,我再次展现一下该方法。
看源码以前,咱们得冷静的分析。目标要十分明确,就是想要找到Dubbo最小活跃数算法的具体实现类以及实现类的具体逻辑是什么。根据咱们的provider.xml里面的:
很明显,咱们知道loadbalance是关键字。因此咱们拿着loadbalance全局搜索,能够看到dubbo包下面的LoadBalance。
这是一个SPI接口com.alibaba.dubbo.rpc.cluster.LoadBalance:
其实现类为:
com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance
AbstractLoadBalance是一个抽象类,该类里面有一个抽象方法doSelect。这个抽象方法其中的一个实现类就是咱们要分析的最少活跃次数负载均衡的源码。
同时,到这里。咱们知道了LoadBalance是一个SPI接口,说明咱们能够扩展本身的负载均衡策略。抽象方法doSelect有四个实现类。这个四个实现类,就是Dubbo官方提供的负载均衡策略,他们分别是:
ConsistentHashLoadBalance 一致性哈希算法
LeastActiveLoadBalance 最小活跃数算法
RandomLoadBalance 加权随机算法
RoundRobinLoadBalance 加权轮询算法
咱们已经找到了LeastActiveLoadBalance这个类了,那么咱们的第二个断点打在哪里已经很明确了。
目前看来,两个断点就能够支撑咱们的分析了。
有的朋友可能想问,那我想知道Dubbo是怎么识别出咱们想要的是最少活跃次数算法,而不是其余的算法呢?其余的算法是怎么实现的呢?从第一个断点到第二个断点直接有着怎样的调用链呢?
**在没有完全搞清楚最少活跃数算法以前,这些通通先记录在案但不予理睬。**必定要明确目标,带着一个问题进来,就先把带来的问题解决了。以后再去解决在这个过程当中碰到的其余问题。**在这样环环相扣解决问题的过程当中,你就慢慢的把握了源码的精髓。**这是我我的的一点看源码的心得。供诸君参考。
既然叫作最小活跃数策略。那咱们得让现有的三个消费者都有一些调用次数。因此咱们得改造一下服务提供者和消费者。
服务提供者端的改造以下:
PS:这里以权重为300的服务端为例。另外的两个服务端改造点相同。
客户端的改造点以下(PS:for循环中i应该是<20):
一共发送21个请求:其中前20个先发到服务端让其hold住(由于服务端有sleep),最后一个请求就是咱们须要Debug跟踪的请求。
运行一下,让程序停在断点的地方,而后看看控制台的输出:
权重为300的服务端共计收到9个请求
权重为200的服务端共计收到6个请求
默认权重的服务端共计收到5个请求
咱们还有一个请求在Debug。直接进入到咱们的第二个断点的位置,并Debug到下图所示的一行代码(能够点看查看大图):
正如上面这图所说的:weight=100回答了一个问题,active=0提出的一个问题。
weight=100回答了什么问题呢?
默认权重是多少?是100。
咱们服务端的活跃数分别应该是下面这样的
权重为300的服务端,active=9
权重为200的服务端,active=6
默认权重(100)的服务端,active=5
可是这里为何active会等于0呢?这是一个问题。
继续往下Debug你会发现,每个服务端的active都是0。因此相比之下没有一个invoker有最小active。因而程序走到了根据权重选择invoker的逻辑中。
active为0说明在dubbo调用的过程当中active并无发生变化。那active为何是0,其实就是在问active何时发生变化?
要回答这个问题咱们得知道active是在哪里定义的,由于在其定义的地方,必有其修改的方法。
下面这图说明了active是定义在RpcStatus类里面的一个类型为AtomicInteger的成员变量。
在RpcStatus类中,有三处()调用active值的方法,一个增长、一个减小、一个获取:
很明显,咱们须要看的是第一个,在哪里增长。
因此咱们找到了beginCount(URL,String)方法,该方法只有两个Filter调用。ActiveLimitFilter,见名知意,这就是咱们要找的东西。
com.alibaba.dubbo.rpc.filter.ActiveLimitFilter具体以下:
看到这里,咱们就知道怎么去回答这个问题了:**为何active是0呢?由于在客户端没有配置ActiveLimitFilter。**因此,ActiveLimitFilter没有生效,致使active没有发生变化。
怎么让其生效呢?已经呼之欲出了。
好了,再来试验一次:
加上Filter以后,咱们经过Debug能够看到,对应权重的活跃数就和咱们预期的是一致的了。
权重为300的活跃数为6
权重为200的活跃数为11
默认权重(100)的活跃数为3
根据活跃数咱们能够分析出来,最后咱们Debug住的这个请求,必定会选择默认权重的invoker去执行,由于他是当前活跃数最小的invoker。以下所示:
虽然到这里咱们还没开始进行源码的分析,只是把流程梳理清楚了。可是把Demo完整的搭建了起来,并且知道了最少活跃数负载均衡算法必须配合ActiveLimitFilter使用,位于RpcStatus类的active字段才会起做用,不然,它就是一个基于权重的算法。
比起其余地方直接告诉你,要配置ActiveLimitFilter才行哦,咱们本身实验得出的结论,能让咱们的印象更加深入。
咱们再仔细看一下加上ActiveLimitFilter以后的各个服务的活跃数状况:
权重为300的活跃数为6
权重为200的活跃数为11
默认权重(100)的活跃数为3
你不以为奇怪吗,为何权重为200的活跃数是最高的?
其在业务上的含义是:咱们有三台性能各异的服务器,A服务器性能最好,因此权重为300,B服务器性能中等,因此权重为200,C服务器性能最差,因此权重为100。
当咱们选择最小活跃次数的负载均衡算法时,咱们指望的是性能最好的A服务器承担更多的请求,而真实的状况是性能中等的B服务器承担的请求更多。这与咱们的设定相悖。
若是你说20个请求数据量太少,多是巧合,不足以说明问题。说明你还没被我带偏,咱们不能基于巧合编程。
因此为了验证这个地方确实有问题,我把请求扩大到一万个。
同时,记得扩大provider端的Dubbo线程池:
因为每一个服务端运行的代码都是同样的,因此咱们指望的结果应该是权重最高的承担更多的请求。可是最终的结果如图所示:
各个服务器均摊了请求。这就是我文章最开始的时候说的Dubbo 2.6.0版本中最小活跃数负载均衡算法的Bug之一。
接下来,咱们带着这个问题,去分析源码。
com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance的源码以下,我逐行进行了解读。能够点开查看大图,细细品读,很是爽:
下图中红框框起来的部分就是一个基于权重选择invoker的逻辑:
我给你们画图分析一下:
请仔细分析图中给出的举例说明。同时,上面这图也是按照比例画的,能够直观的看到,对于某一个请求,区间(权重)越大的服务器,就越可能会承担这个请求。因此,当请求足够多的时候,各个服务器承担的请求数,应该就是区间,即权重的比值。
其中第81行有调用getWeight方法,位于抽象类AbstractLoadBalance中,也须要进行重点解读的代码。
com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance的源码以下,我也进行了大量的备注:
在AbstractLoadBalance类中提到了一个预热的概念。官网中是这样的介绍该功能的:
权重的计算过程主要用于保证当服务运行时长小于服务预热时间时,对服务进行降权,避免让服务在启动之初就处于高负载状态。服务预热是一个优化手段,与此相似的还有 JVM 预热。主要目的是让服务启动后“低功率”运行一段时间,使其效率慢慢提高至最佳状态。
从上图代码里面的公式(演变后):计算后的权重=(uptime/warmup)*weight能够看出:随着服务启动时间的增长(uptime),计算后的权重会愈来愈接近weight。从实际场景的角度来看,随着服务启动时间的增长,服务承担的流量会慢慢上升,没有一个陡升的过程。因此这是一个优化手段。同时Dubbo接口还支持延迟暴露。
在仔细的看完上面的源码解析图后,配合官网的总结加上个人灵魂画做,相信你能够对最小活跃数负载均衡算法有一个比较深刻的理解:
1.遍历 invokers 列表,寻找活跃数最小的 Invoker
2.若是有多个 Invoker 具备相同的最小活跃数,此时记录下这些 Invoker 在 invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等
3.若是只有一个 Invoker 具备最小的活跃数,此时直接返回该 Invoker 便可
4.若是有多个 Invoker 具备最小活跃数,且它们的权重不相等,此时处理方式和 RandomLoadBalance 一致
5.若是有多个 Invoker 具备最小活跃数,但它们的权重相等,此时随机返回一个便可
因此我以为最小活跃数负载均衡的全称应该叫作:有最小活跃数用最小活跃数,没有最小活跃数根据权重选择,权重同样则随机返回的负载均衡算法。
Dubbo2.6.0最小活跃数算法Bug一
问题出在标号为①和②这两行代码中:
标号为①的代码在url中取出的是没有通过getWeight方法降权处理的权重值,这个值会被累加到权重总和(totalWeight)中。
标号为②的代码取的是通过getWeight方法处理后的权重值。
取值的差别会致使一个问题,标号为②的代码的左边,offsetWeight是一个在[0,totalWeight)范围内的随机数,右边是通过getWeight方法降权后的权重。因此在通过leastCount次的循环减法后,offsetWeight在服务启动时间还没到热启动设置(默认10分钟)的这段时间内,极大可能仍然大于0。致使不会进入到标号为③。直接到标号为④码处,变成了随机调用策略。这与设计不符,因此是个bug。
前面章节说的状况就是这个Bug致使的。
这个Bug对应的issues地址和pull request分为:
那怎么修复的呢?咱们直接对比Dubbo 2.7.4.1(目前最新版本)的代码:
能够看到获取weight的方法变了:从url中直接获取变成了经过getWeight方法获取。获取到的变量名称也变了:从weight变成了afterWarmup,更加的见名知意。
还有一处变化是获取随机值的方法的变化,从Randmo变成了ThreadLoaclRandom,性能获得了提高。这处变化就不展开讲了,有兴趣的朋友能够去了解一下。
Dubbo2.6.0最小活跃数算法Bug二
这个Bug我没有遇到,可是我在官方文档上看了其描述(官方文档中的版本是2.6.4),引用以下:
官网上说这个问题在2.6.5版本进行修复。我对比了2.6.0/2.6.5/2.7.4.1三个版本,发现每一个版本都略有不一样。以下所示:
图中标记为①的三处代码:
2.6.0版本的是有Bug的代码,缘由在上面说过了。
2.6.5版本的修复方式是获取随机数的时候加一,因此取值范围就从[0,totalWeight)变成了[0,totalWeight],这样就能够避免这个问题。
2.7.4.1版本的取值范围仍是[0,totalWeight),可是它的修复方法体如今了标记为②的代码处。2.6.0/2.6.5版本标记为②的地方都是if(offsetWeight<=0),而2.7.4.1版本变成了if(offsetWeight<0)。
你品一品,是否是效果是同样的,可是更加优雅了。
朋友们,魔鬼,都在细节里啊!
在看官网文档负载均衡介绍的时候。发现了一处笔误。因此我对其进行了修改并被merged。
能够看到,改动点也是一个很是小的地方。可是,我也为Dubbo社区贡献了一份本身的力量。我是Dubbo文档的committer,简称"Dubbo committer"。
本小节主要是简单的介绍一下给开源项目提pr的流程。
首先,fork项目到本身的仓库中。而后执行如下命令,拉取项目并设置源:
git clone github.com/thisiswangh…
cd dubbo-website
git remote add upstream github.com/apache/dubb…
git remote set-url --push upstream no_push
建立本地分支:
git checkout -b xxxx
开发完成后提交代码:
git fetch upstream
git checkout master
git merge upstream/master git checkout -b xxxx git rebase master git push origin xxxx:xxxx
而后到git上建立pull request后,静候通知。
以前也写过Dubbo的文章《Dubbo 2.7新特性之异步化改造》,经过对比Dubbo2.6.0/2.7.0/2.7.3版本的源码,分析Dubbo2.7 异步化的改造的细节,能够看看哦。
才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
感谢您的阅读,个人订阅号里全是原创,十分欢迎并感谢您的关注。
以上。