
dubbo随机负载均衡的权重很少会用到吗?之前我想给随机负载均衡策略配置权重,各种搜索都找不到答案,包括翻阅官方文档。而且我们项目中用的还是最新的Nacos注册中心,非常无奈,最后只能在源码中寻找答案。
关于负载均衡权限的使用场景,我会在下篇文章介绍。那么,本篇将会解答读者两个疑问,其实是我自己的疑问。第一个,使用Nacos作为注册中心如何修改负载均衡策略的权重;第二个,通过阅读源码,找出权重是在何时起作用的,以及介绍随机负载均衡策略的具体实现。主要是为了理解权重是怎么起作用的。
我似乎在每篇文章中都强调一次URL在dubbo中的地位,URL携带了太多信息。负载均衡策略、权重、超时时间、应用启动是否检查提供者可用、长连接数等,这些统称为元数据。spring与dubbo整合时,会将配置信息附加到URL上。注意,此URL非java.net.URL。
dubbo的配置会有优先级问题,最简单的理解,它不仅可以在服务提供端配置,也可在服务消费端配置,反正最后都是通过URL传递的。但是,负载均衡的权重配置如果是动态修改,那就只能在服务提供端配置。为什么呢?
负载均衡是在消费端实现的,权重起作用肯定也是在消费端。而动态修改元数据只有订阅者会收到,因为服务提供者不订阅消费者。回顾下dubbo的架构图。

从官方提供的架构图就可以看出,服务提供者只会注册到注册中心,它并不会订阅服务消费者的变更,所以,不管你是修改服务提供者还是服务消费者的元数据,服务提供者都不得而知。
第一步:打开nacos管理后台,找到需要调整权重的服务,注意,找服务提供者。然后点击详情。

第二步:为每个提供者调整权重,在操作列点击编辑按钮。

第三步:修改权重,完事

这也太简单了,都不用去动元数据,当然,也可以在元数据中修改。至于key是什么,得从源码中找了。
吸取上次的教训,这次不再绕进dubbo的源码实现细节中。站在宏观的角度去分析整个动态修改权重的流程。注册中心是很核心的一个模块,我打算后续单独写一篇介绍注册中心。
public interface RegistryService {void register(URL url);void unregister(URL url);void subscribe(URL url, NotifyListener listener);void unsubscribe(URL url, NotifyListener listener);List<URL> lookup(URL url);}
register方法:服务提供者在导出服务时或者服务消费者在引入服务时,都需要将自己注册到注册中心。
unregister方法:如果我们使用kill -9 pid方式停止服务,那么这个方法就永远都得不到执行。它是用来解除注册的,告诉注册中心,我要下线了。这样其它消费者才不会再调用这个不可用的服务。
subscribe方法:该方法只有消费者会用到,消费者订阅服务提供者变更事件,从而同步提供者到本地。
unsubscribe方法:与subscribe方法相对,解除订阅,在消费者下线之前调用,告知注册中心。
lookup方法:全量从注册中心拉取一次提供者,通常是服务启动时。如果拉取失败则使用本地持久化文件中的。
接着是Directory接口,Dubbo的服务目录简单说就是消费者将自己能够调用的服务提供者的信息缓存到本地Directory中。当消费者接收到注册中心发来的相关服务提供者变动消息时,会更新本地服务目录Directory。使用本地目录的好处,笔者想到的有两点:
不应该让注册中心承受高并发请求,且免去远程调用获取服务提供者的耗时。如果每个请求都要向注册中心询问服务提供者,那么注册中心的并发量将是所有提供者的总和。
避免因注册中心挂掉时,无法获取到提供者,导致服务奔溃。
public interface Directory<T> extends Node {Class<T> getInterface();List<Invoker<T>> list(Invocation invocation) throws RpcException;}
其中,list方法就是获取所有可用的服务提供者。
我们还要关心一个NotifyListener接口,RegistryService的subscribe和unsubscribe都需要传递一个类型为NotifyListener的参数。来看下NotifyListener接口的定义。
public interface NotifyListener {void notify(List<URL> urls);}
当注册中心监控到有服务提供者变更时,会通知消费者,回调方法就是notify,参数是变更后的所有服务提供者。Directory本地服务目录的刷新也是由notify调用的。所以,当我们在nacos注册中心修改服务提供者的元数据时,会触发每个服务订阅者的notify方法,这样我们修改的信息就可以生效了。
我们能修改的所有元数据信息(配置),都在URL中。接着我们看负载均衡策略的实现,主要是分析随机负载均衡策略。
@SPI(RandomLoadBalance.NAME)public interface LoadBalance {@Adaptive("loadbalance")<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;}
负载均衡用到了SPI自适应扩展点机制@Adaptive,当未配置loadbalance时,默认使用RandomLoadBalance.NAME,即随机负载均衡策略。
select方法:就是要我们从所有可用的服务提供者invokers中选择一个提供者调用,该方法在消费端每次发起rpc远程调用之前被调用一次。当然,是在不考虑重试的情况下。
random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalanceroundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalanceleastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalanceconsistenthash=org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
只有通过分析RandomLoadBalance的select实现,我们才能知道权重是怎么起作用的,以及权重是怎么拿到的。

RandomLoadBalance继承自AbstractLoadBalance,那么我们先看其父类。我们我不研究太多细节,所以我把源码简化了一下。
@Overridepublic <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {if (CollectionUtils.isEmpty(invokers)) {return null;}if (invokers.size() == 1) {return invokers.get(0);}return doSelect(invokers, url, invocation);}protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
AbstractLoadBalance实现select方法,做的事情非常简单,当提供者只有一个的时候,不走任何的负载均衡实现,直接返回。只有服务提供者大于1时,才调用doSelect,真正的负载均衡实现逻辑交由子类实现。同时AbstractLoadBalance还实现了一个通用的方法,就是获取权重。
protected int getWeight(Invoker<?> invoker, Invocation invocation) {weight是从服务提供者的URL参数中获取的,所以动态修改我们可以在nacos注册中心中修改服务提供者的权重参数即可生效由于消费者订阅注册中心事件,接收到事件后会更新本地服务目录,权重就可以生效。int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);......未配置则权重为0return weight >= 0 ? weight : 0;}
invoker是服务提供者,从服务提供者的URL中获取weight参数,如果获取不到,则取默认值100。这也说通了为什么权重只有在服务提供端配置才起作用。
RandomLoadBalance随机负载均衡的具体实现。
@Overrideprotected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {int length = invokers.size();....在权重都相等的情况下,直接随机获取一个提供者return invokers.get(ThreadLocalRandom.current().nextInt(length));}
省略号处我们分三部份分析。第一部分:
boolean sameWeight = true;int[] weights = new int[length];int firstWeight = getWeight(invokers.get(0), invocation);weights[0] = firstWeight;int totalWeight = firstWeight;
sameWeight:用于标志是否每个提供者的权重都相等,如果权重都相等,就直接随机获取一个提供者就行;
weights:存储每个提供者的权重;
firstWeight:第一个提供者的权重,用于判断每个提供者是否都与第一个提供的权重相等,只要有一个不相等则sameWeight为false;
totalWeight:所有提供者权重的总和。
第二部分
第一个提供者已经获取,遍历只需获取剩余的提供者的权重for (int i = 1; i < length; i++) {int weight = getWeight(invokers.get(i), invocation);weights[i] = weight;totalWeight += weight;// 如果当前提供者的权重与第一个不等,则sameWeight为false// 其实这里可以优化一下,不等就直接跳出循环if (sameWeight && weight != firstWeight) {sameWeight = false;}}
从第二个服务提供者开始,遍历获取每个提供者的权重,如果发现有一个提供者的权重不等于第一个提供者,则将sameWeight置为false,表示权重生效。
第三部分:
if (totalWeight > 0 && !sameWeight) {int offset = ThreadLocalRandom.current().nextInt(totalWeight);for (int i = 0; i < length; i++) {offset -= weights[i];if (offset < 0) {return invokers.get(i);}}}
当总的权重大于0且每个提供者的权重都不相同的情况下,才根据权重随机获取调用者。offset为取0到总权重之间的一个随机数。
假设现有三个提供者A、B、C ,权重分别配置为
A: 30、B:30、C:60
总和为100
那么offset是取0~100之间的一个随机数。接着遍历所有提供者,用offset减去提供者的权重,如果offset小于0则取当前提供者。其原理就是分段,a取 0~30,b取30~60,c取 61~100 。如果随机数在a区间则使用提供者a,如果随机数在b区间,则取提供者b,否则落在c区间就取提供者c 。只不过这里用了减法实现。
推理一遍:假设随机数offset为54
第一次循环:offset减去a提供者的权重30 ,结果大于0,此时offset为54-30=24;
第二次循环:offset减去b提供者的权重30 ,结果为24-30=-6,小于0,确认过眼神,就是第二个提供者了。
了解源码之后,不管使用任务配置中心,我们都能动态修改负载均衡策略及权重,甚至是其它的配置。
有时候,通过修改服务提供者元数据方式去动态修改负载均衡策略并不能满足我们的需求,因为每次服务提供者重启配置就都还原了。所以,最好是能够通过配置中心方式去动态修改负载均衡策略的权重,如果能提供算法让消费者自己去计算权重那就更好了。
为解决服务重启后权重重置的问题,下篇介绍如何通过配置中心和自实现随机负载均衡策略实现权重动态修改,以及为解除繁琐配置而实现的一种自适应权重算法。




