美文网首页技术
扩展Ribbon支持Nacos权重的三种方式

扩展Ribbon支持Nacos权重的三种方式

作者: 初心myp | 来源:发表于2019-08-15 15:46 被阅读0次

    Nacos支持权重配置,这是个比较实用的功能,例如:

    • 把性能差的机器权重设低,性能好的机器权重设高,让请求优先打到性能高的机器上去;
    • 某个实例出现异常时,把权重设低,排查问题,问题排查完再把权重恢复;
    • 想要下线某个实例时,可先将该实例的权重设为0,这样流量就不会打到该实例上了——此时再去关停该实例,这样就能实现优雅下线啦。当然这是为Nacos量身定制的优雅下线方案——Spring Cloud中,要想实现优雅下线还有很多姿势,详见:《实用技巧:Spring Cloud中,如何优雅下线微服务?》 ,里面笔者总结了四种优雅下线的方式。

    然而测试发现,Nacos权重配置对Spring Cloud Alibaba无效。也就是说,不管在Nacos控制台上如何配置,调用时都不管权重设置的。

    Spring Cloud Alibaba通过整合Ribbon的方式,实现了负载均衡。所使用的负载均衡规则是 ZoneAvoidanceRule

    本节来探讨如何扩展Ribbon,让其支持Nacos的权重配置,笔者总结了三种方案。

    方案1:自己实现负载均衡规则

    思路

    自己首先一个Ribbon负载均衡规则就可以了。

    • 权重配置啥的,都可以在实例信息中获取到。
    • 自己基于权重配置,计算出一个实例即可。

    代码

    @Slf4j
    public class NacosWeightRandomV1Rule extends AbstractLoadBalancerRule {
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
        }
    
        @Override
        public Server choose(Object key) {
            List<Server> servers = this.getLoadBalancer().getReachableServers();
    
            List<InstanceWithWeight> instanceWithWeights = servers.stream()
                    .map(server -> {
                        // 注册中心只用Nacos,没同时用其他注册中心(例如Eureka),理论上不会实现
                        if (!(server instanceof NacosServer)) {
                            log.error("参数非法,server = {}", server);
                            throw new IllegalArgumentException("参数非法,不是NacosServer实例!");
                        }
    
                        NacosServer nacosServer = (NacosServer) server;
                        Instance instance = nacosServer.getInstance();
                        double weight = instance.getWeight();
                        return new InstanceWithWeight(
                                server,
                                Double.valueOf(weight).intValue()
                        );
                    })
                    .collect(Collectors.toList());
    
            Server server = this.weightRandom(instanceWithWeights);
    
            log.info("选中的server = {}", server);
            return server;
        }
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        private class InstanceWithWeight {
            private Server server;
            private Integer weight;
        }
    
        /**
         * 根据权重随机
         * 算法参考 https://blog.csdn.net/u011627980/article/details/79401026
         *
         * @param list 实例列表
         * @return 随机出来的结果
         */
        private Server weightRandom(List<InstanceWithWeight> list) {
            List<Server> instances = Lists.newArrayList();
            for (InstanceWithWeight instanceWithWeight : list) {
                int weight = instanceWithWeight.getWeight();
                for (int i = 0; i <= weight; i++) {
                    instances.add(instanceWithWeight.getServer());
                }
            }
            int i = new Random().nextInt(instances.size());
            return instances.get(i);
        }
    }
    
    

    WARNING

    本段代码存在优化空间,只是用来演示思考的过程,不建议用于生产,如打算使用本方案实现,请参考以下两点优化

    • 简单起见,我直接把double型的权重(weight),转成了integer计算了,存在精度丢失
    • InstanceWithWeight太重了,在 weightRandom 还得再两层for循环,还挺吃内存的,建议百度其他权重随机算法优化。不过实际项目一个微服务一般也就三五个实例,所以其实内存消耗也能忍受。不优化问题也不大。

    方案2:利用Nacos Client的能力[推荐]

    思路

    在阅读代码Nacos源码的过程中,发现Nacos Client本身就提供了负载均衡的能力,并且负载均衡算法正是我们想要的根据权重选择实例

    代码在 com.alibaba.nacos.api.naming.NamingService#selectOneHealthyInstance ,只要想办法调用到这行代码,就可以实现我们想要的功能啦!

    代码

    @Slf4j
    public class NacosWeightRandomV2Rule extends AbstractLoadBalancerRule {
        @Autowired
        private NacosDiscoveryProperties discoveryProperties;
    
        @Override
        public Server choose(Object key) {
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            String name = loadBalancer.getName();
            try {
                Instance instance = discoveryProperties.namingServiceInstance()
                        .selectOneHealthyInstance(name);
    
                log.info("选中的instance = {}", instance);
    
                /*
                 * instance转server的逻辑参考自:
                 * org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList.instancesToServerList
                 */
                return new NacosServer(instance);
            } catch (NacosException e) {
                log.error("发生异常", e);
                return null;
            }
        }
    
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
        }
    }
    
    

    方案3:最暴力的玩法

    思路

    在阅读源码的过程中,发现如下代码:

    // 来自:org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList#getServers
    private List<NacosServer> getServers() {
      try {
        List<Instance> instances = discoveryProperties.namingServiceInstance()
          .selectInstances(serviceId, true);
        return instancesToServerList(instances);
      }
      catch (Exception e) {
        throw new IllegalStateException(
          "Can not get service instances from nacos, serviceId=" + serviceId,
          e);
      }
    }
    
    

    这个NacosServerList 就是给Ribbon去做负载均衡的”数据源”!如果把这里的代码改成 com.alibaba.nacos.api.naming.NamingService#selectOneHealthyInstance 不也可以实现我们想要的功能吗?

    也就是说,交给Ribbon的List永远只有1个实例!这样不管Ribbon用什么负载均衡,都随他便了。

    代码

    1 参考NacosServerList的代码,重写NacosRibbonServerList

    /**
     * 参考org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList
     */
    @Slf4j
    public class NacosRibbonServerList extends AbstractServerList<NacosServer> {
    
        private NacosDiscoveryProperties discoveryProperties;
    
        private String serviceId;
    
        public NacosRibbonServerList(NacosDiscoveryProperties discoveryProperties) {
            this.discoveryProperties = discoveryProperties;
        }
    
        @Override
        public List<NacosServer> getInitialListOfServers() {
            return getServers();
        }
    
        @Override
        public List<NacosServer> getUpdatedListOfServers() {
            return getServers();
        }
    
        private List<NacosServer> getServers() {
            try {
                Instance instance = discoveryProperties.namingServiceInstance()
                        .selectOneHealthyInstance(serviceId, true);
                log.debug("选择的instance = {}", instance);
                return instancesToServerList(
                        Lists.newArrayList(instance)
                );
            } catch (Exception e) {
                throw new IllegalStateException(
                        "Can not get service instances from nacos, serviceId=" + serviceId,
                        e);
            }
        }
    
        private List<NacosServer> instancesToServerList(List<Instance> instances) {
            List<NacosServer> result = new ArrayList<>();
            if (null == instances) {
                return result;
            }
            for (Instance instance : instances) {
                result.add(new NacosServer(instance));
            }
            return result;
        }
    
        public String getServiceId() {
            return serviceId;
        }
    
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
            this.serviceId = iClientConfig.getClientName();
        }
    }
    
    

    2 编写配置类

    /**
     * 参考:org.springframework.cloud.alibaba.nacos.ribbon.NacosRibbonClientConfiguration
     */
    @Configuration
    public class NacosRibbonClientExtendConfiguration {
        @Bean
        public ServerList<?> ribbonServerList(IClientConfig config, NacosDiscoveryProperties nacosDiscoveryProperties) {
            NacosRibbonServerList serverList = new NacosRibbonServerList(nacosDiscoveryProperties);
            serverList.initWithNiwsConfig(config);
            return serverList;
        }
    }
    
    

    3 添加注解,让上面的NacosRibbonClientExtendConfiguration成为Ribbon的默认配置

    // ...其他注解
    @RibbonClients(defaultConfiguration = NacosRibbonClientExtendConfiguration.class)
    public class ConsumerMovieApplication {
      public static void main(String[] args) {
        SpringApplication.run(ConsumerMovieApplication.class, args);
      }
    }
    
    

    注意

    务必注意,将 NacosRibbonClientExtendConfiguration 放在ComponentScan上下文(默认是启动类所在包及其子包)以外!!!

    总结与对比

    • 方案1:是最容易想到的玩法。
    • 方案2:是个人目前最喜欢的方案。首先简单,并且都是复用Nacos/Ribbon现有的代码——而Ribbon/Nacos本身都是来自于大公司生产环境,经过严苛的生产考验。
    • 方案3:太暴力了,把Ribbon架空了。此方案中,扔给Ribbon做负载均衡选择时,List只有1个元素,不管用什么算法去算,最后总是会返回这个元素!

    思考

    既然Nacos Client已经有负载均衡的能力,Spring Cloud Alibaba为什么还要去整合Ribbon呢?

    个人认为,这主要是为了符合Spring Cloud标准。Spring Cloud Commons有个子项目 spring-cloud-loadbalancer ,该项目制定了标准,用来适配各种客户端负载均衡器(虽然目前实现只有Ribbon,但Hoxton就会有替代的实现了)。

    Spring Cloud Alibaba遵循了这一标准,所以整合了Ribbon,而没有去使用Nacos Client提供的负载均衡能力。

    相关文章

      网友评论

        本文标题:扩展Ribbon支持Nacos权重的三种方式

        本文链接:https://www.haomeiwen.com/subject/fypzjctx.html