Spring Cloud Alibaba - 扩展Ribbon支持Nacos权重

1,138 阅读6分钟

🌈往期回顾

第一期:Spring Cloud Alibaba前景如何?

第二期:Spring Cloud Alibaba Nacos服务治理

第三期:Spring Cloud Alibaba Sentinel - 分布式系统的流量防卫兵

第四期:Spring Cloud Alibaba Ribbon - 负载均衡

第五期:Spring Cloud Alibaba Gateway - 服务网关

Nacos支持权重配置,是一个实现负载均衡的一个比较实用的功能,例如:

✏把性能差的机器权重设低,性能好的机器权重设高,让请求优先打到性能高的机器上去,某个实例出现异常时,把权重设低,排查问题,问题排查完再把权重恢复,想要下线某个实例时,可先将该实例的权重设为0,这样流量就不会打到该实例上了——此时再去关停该实例,也就是所谓的实现优雅下线功能。

目前,Nacos权重配置对SpringCloudAlibaba无效,也就是说,不管在Nacos控制台如何配置权重大小,调用时都不会生效的,然后SpringCloudAlibaba可以通过整合Ribbon的方式,实现负载均衡,默认使用的负载均衡规则是:ZoneAvoidanceRule,该篇文章主要是讲解一下扩展Ribbon支持Nacos权重的集中方式和探讨SpringCloudAlibaba如何扩展Ribbon,让其支持Nacos权重配置。

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

  1. 自定义实现负载均衡规则
  2. 利用Nacos Client原身的能力【推荐】

1️⃣自定义实现负载均衡规则

  • 实现思路:

通过自定义自己的一个权重负载均衡规则NacosWeightRule,通过继承抽象类AbstractLoadBalancerRule,重写choose(Object key) 方法。

/**
 * 通过重写choose方法,实现基于权重的负载均衡算法
 *
 * @author: jacklin
 * @since 2022/6/2 20:16
 **/
@Override
public Server choose(Object key) {
    List<Server> serverList = this.getLoadBalancer().getReachableServers();
    List<InstanceWithWeight> instanceWithWeights = serverList.stream().map(server -> {
        if (!(server instanceof NacosServer)) {
            log.error("参数非法,server = {}", server);
            throw new IllegalArgumentException("参数非法,不是NacosService实例!");
        }

        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.chooseWithWeightRandom(instanceWithWeights);
    log.info("被选中的server = {}", server);

    return server;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
private class InstanceWithWeight {
    private Server server;
    private Integer weight;
}

/**
 * 根据权重随机获取Server实例
 *
 * @param list 实例列表
 * @return 随机筛选的实例
 **/
private Server chooseWithWeightRandom(List<InstanceWithWeight> list) {
    List<Server> instances = Lists.newArrayList();
    for (InstanceWithWeight instanceWithWeight : list) {
        Integer 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);
}

实现方式二其实存在一定的优化空间,只是用来演示思考的过程,不建议生产使用,如果打算使用本方案,请参考以下两点优化:

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

算法:如何实现带权重的随机选择?

2️⃣利用Nacos Client原身的能力【推荐】

阅览Nacos源码可以发现,Nacos Client本身就提供了负载均衡的能力,并且负载均衡算法正是我们想要的根据权重选择实例,源代码位置:com.alibaba.nacos.api.naming.NamingService#selectOneHealthyInstance(java.lang.String),只需要想办法调用到这行代码,既可以实现我们想要的功能enenen!!!

package com.jacklin.mamba.contentcenter.post.config;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Ribbon支持Nacos权重配置,通过继承抽象类 AbstractLoadBalancerRule 实现抽象方法
 *
 * @author jacklin
 * @Date: 2022-01-06 22:47
 */
@Slf4j
public class NacosWeightRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Autowired
    private NacosServiceManager nacosServiceManager;

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        //读取配置文件,并初始化NacosWeightRule

    }

    //通过重写choose方法,实现基于权重的负载均衡算法
    @Override
    public Server choose(Object key) {
        try {
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            log.info("loadBalancer:{}", loadBalancer);
            //想要请求的微服务名称
            String name = loadBalancer.getName();
            // 实现一个基于权重的负载均衡算法,事实上Nacos Client已经实现了对应的权重负载均衡算法,先拿到服务发现的相关API
            NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());
            // Nacos Client自动通过基于权重的负载均衡算法,给我们选择的一个实例
            Instance instance = namingService.selectOneHealthyInstance(name);
            log.info("选择的实例是:instance:{}, port:{}", instance, instance.getPort());
            return new NacosServer(instance);

        } catch (Exception e) {
            return null;
        }
    }
}

RestTemplateConfig

package com.jacklin.mamba.contentcenter.post.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * 配置RestTemplate
 *
 * @author jacklin
 * @Date: 2021-12-30 22:24
 */
@Configuration
public class RestTemplateConfig {

    /**
     * 为restTemplate整合ribbon
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * Ribbon 自带的负载均衡策略有如下几个:
     * 1.AvailabilityFilteringRule: 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,以及并发连接数超过阈值的服务,剩下的服务,使用轮询策略
     * 2.BestAvailableRule: 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
     * 3.ZoneAvoidanceRule: 复合判断 server 所在区域的性能和 server 的可用性选择服务器
     * 4.RandomRule: 随机负载均衡
     * 5.RoundRibbonRule:轮询,人人有份,一个个来
     * 6.RetryRule:先轮询,如果获取失败则在指定时间内重试,重新轮询可用的服务
     * 7.WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应越快的服务权重越高,越容易被选中。一开始启动时,统计信息不足的情况下,使用轮询
     */
    @Bean
    public IRule myCustomRule() {
        //轮询策略负载均衡算法,人人有份,一个个来
        //return new RoundRobinRule();
        //return new RandomRule();
        return new NacosWeightRule();
    }
}

自定义一个myCustomRule,选择我们的NacosWeightRule,既可以实现按权重去调用不同实例啦~

image.png

通过在Nacos控制台实现user-center不同实例的权重大小,将8081端口实例权重设置为0,8080端口权重设置为1,请求观察控制台日志打印:

image.png

调用接口请求10次,发现请求都打到8080端口:

image.png

content-center服务日志输出: image.png

🎉🎉🎉很显然我们按权重负载均衡算法已经得以实现!

💨总结

方式一是最容易想到的玩法。方式二是个人目前最喜欢的方案。首先简单,并且都是复用Nacos/Ribbon现有的代码——而Ribbon/Nacos本身都是来自于大公司生产环境,经过严苛的生产考验。

❓思考

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

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

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