源码分析Dubbo负载均衡策略的权重如何动态修改

原创 吴就业 116 0 2019-11-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/1e60388cd10b426d9148592c7f809dd7

作者:吴就业
链接:https://wujiuye.com/article/1e60388cd10b426d9148592c7f809dd7
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2019年11月22日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

dubbo随机负载均衡的权重很少会用到吗?之前我想给随机负载均衡策略配置权重,各种搜索都找不到答案,包括翻阅官方文档。而且我们项目中用的还是最新的Nacos注册中心,非常无奈,最后只能在源码中寻找答案。

关于负载均衡权限的使用场景,我会在下篇文章介绍。那么,本篇将会解答读者两个疑问,其实是我自己的疑问。第一个,使用Nacos作为注册中心如何修改负载均衡策略的权重;第二个,通过阅读源码,找出权重是在何时起作用的,以及介绍随机负载均衡策略的具体实现。主要是为了理解权重是怎么起作用的。

先抛出一个问题,为什么要动态修改?一个服务我们是以集群方式部署的,至于部署多少个节点,以及部署在哪台机器上是无法确定的,因此没办法在代码中写死配置,只能通过动态修改。只要用到负载均衡策略的权重,就避不开动态修改权重的问题。

Nacos注册中心如何修改权重

我似乎在每篇文章中都强调一次URL在dubbo中的地位,URL携带了太多信息。负载均衡策略、权重、超时时间、应用启动是否检查提供者可用、长连接数等,这些统称为元数据。spring与dubbo整合时,会将配置信息附加到URL上。注意,此URL非java.net.URL。

dubbo的配置会有优先级问题,最简单的理解,它不仅可以在服务提供端配置,也可在服务消费端配置,反正最后都是通过URL传递的。但是,负载均衡的权重配置如果是动态修改,那就只能在服务提供端配置。为什么呢?

负载均衡是在消费端实现的,权重起作用肯定也是在消费端。而动态修改元数据只有订阅者会收到,因为服务提供者不订阅消费者。回顾下dubbo的架构图。

图片

从官方提供的架构图就可以看出,服务提供者只会注册到注册中心,它并不会订阅服务消费者的变更,所以,不管你是修改服务提供者还是服务消费者的元数据,服务提供者都不得而知。

同样的,消费者只对提供者的变更感兴趣,它并不会拉取每个消费者的。再者,后面通过阅读源码你就会发现,权重是从提供者的URL中取的。所以,权重只能在服务提供端修改,服务消费者通过notify拿到变更后的权重。

第一步:打开nacos管理后台,找到需要调整权重的服务,注意,找服务提供者。然后点击详情。

图片

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

图片

第三步:修改权重,完事

图片

这也太简单了,都不用去动元数据,当然,也可以在元数据中修改。至于key是什么,得从源码中找了。

源码分析动态修改权重的整个流程

吸取上次的教训,这次不再绕进dubbo的源码实现细节中。站在宏观的角度去分析整个动态修改权重的流程。注册中心是很核心的一个模块,我打算后续单独写一篇介绍注册中心。

首先要介绍的是RegistryService,它是每个注册中心都要实现的接口。

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);
}

接着是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,即随机负载均衡策略。

random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

只有通过分析RandomLoadBalance的select实现,我们才能知道权重是怎么起作用的,以及权重是怎么拿到的。

图片

RandomLoadBalance继承自AbstractLoadBalance,那么我们先看其父类。我们我不研究太多细节,所以我把源码简化了一下。

@Override
    public <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);
        ...... 
        // 未配置则权重为0
        return weight >= 0 ? weight : 0;
}

invoker是服务提供者,从服务提供者的URL中获取*weight*参数,如果获取不到,则取默认值100。这也说通了为什么权重只有在服务提供端配置才起作用。

RandomLoadBalance随机负载均衡的具体实现。

@Override
    protected <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;

第二部分:

// 第一个提供者已经获取,遍历只需获取剩余的提供者的权重
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

总结

了解源码之后,不管使用任务配置中心,我们都能动态修改负载均衡策略及权重,甚至是其它的配置。

有时候,通过修改服务提供者元数据方式去动态修改负载均衡策略并不能满足我们的需求,因为每次服务提供者重启配置就都还原了。所以,最好是能够通过配置中心方式去动态修改负载均衡策略的权重,如果能提供算法让消费者自己去计算权重那就更好了。

为解决服务重启后权重重置的问题,下篇介绍如何通过配置中心和自实现随机负载均衡策略实现权重动态修改,以及为解除繁琐配置而实现的一种自适应权重算法。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

如何让JedisCluster支持Pipeline

pipeline提升性能的关键,一是RTT,节省往返时间,二是I/O系统调用,read系统调用,需要从用户态,切换到内核态。

Redis性能问题如何排查

并发数上升,到底是哪个服务处理能力到了瓶颈,还是Redis性能到了瓶颈,只有找出是哪里的性能问题,才能对症下药。所以,了解redis的一些运维知识能够帮助我们快速判定是否Redis集群的性能问题。

Dubbo自适应随机负载均衡策略的实现

Dubbo默认使用随机负载均衡策略,据笔者了解,目前Dubbo一共提供了四种可选的负载均衡策略,有关于负载均衡策略的实现,如果不怕阅读源码枯燥的,笔者推荐阅读官网的源码导读部份的文档。

深入理解Dubbo源码,Dubbo与Spring的整合之注解方式分析

dubbo通过BeanFactoryPostProcessor与BeanPostProcessor分别完成ServiceBean的注册与被@Reference注释的属性的依赖注入,通过BeanPostProcessor完成配置文件与相关配置类bean的属性绑定。

Fastjson与Jackson性能问题

可以给出的结论是,jackson在解析大json场景下性能是秒杀fastjson的,不急,我们有图有真相。

ES与Redis实现千万级数据的范围查询性能比较,远程 http调用耗时也能降低到0ms

本篇介绍使用ES与使用Redis实现的IP库范围查询谁性能更强,以及远程http调用ip服务耗时如何降低到0ms。