SpringCloudAlibaba-ribbon(生产环境最佳实践)

Ribbon

Ribbon是客户端的IPC(进程间通信)库,是Netflix开源的客户端侧负载均衡器,提供以下能力:

负载均衡

容错能力

异步和相应模型中的多种协议(http、tcp、udp)支持

缓存和批处理

为什么要使用Ribbon

​ 在我们的系统中(分布式系统),存在服务之间的进程通信;而在未使用Ribbon之前,我们需要手动的去实现负载均衡器。Ribbon的引入可以自动的从nacos服务获取服务列表,使用用户指定的负载均衡算法计算出需要调用的实例,使我们简单高效的实现负载均衡,以及Ribbon的扩展性可以满足生产环境中的99.9999%的场景。

负载均衡的两种方式

服务器端负载均衡

ribbon-1.png

客户端侧负载均衡

ribbon-2.png

手动实现客户端侧负载均衡器

未使用负载均衡器

@Override
    public ShareDTO findById(Integer id) {
        // 获取分享详情
        Share share = baseMapper.selectById(id);
        // 获取发布人id
        Integer userId = share.getUserId();

        List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
        String url = instances.stream().map(instance -> instance.getUri().toString() + "/users/{id}").findFirst().orElse("");
        // 远程调用用户中心服务接口
        UserDTO userDTO = restTemplate.getForObject(url, UserDTO.class, userId);

        // 消息的装配
        ShareDTO shareDTO = ShareDTO.builder()
                .wxNickname(userDTO.getWxNickname())
                .build();
        BeanUtils.copyProperties(share, shareDTO);
        return shareDTO;
    }
复制代码

手动实现负载均衡器(随机)

@Override
    public ShareDTO findById(Integer id) {
        // 获取分享详情
        Share share = baseMapper.selectById(id);
        // 获取发布人id
        Integer userId = share.getUserId();

        List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
        List<String> urls = instances.stream().map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList());

        // 随机算法
        int i = ThreadLocalRandom.current().nextInt(urls.size());
        // 远程调用用户中心服务接口
        UserDTO userDTO = restTemplate.getForObject(urls.get(i), UserDTO.class, userId);

        // 消息的装配
        ShareDTO shareDTO = ShareDTO.builder()
                .wxNickname(userDTO.getWxNickname())
                .build();
        BeanUtils.copyProperties(share, shareDTO);
        return shareDTO;
    }
复制代码

Ribbon实现负载均衡

  1. 加依赖(如果使用的nacos discovery,则不需要单独引入ribbon依赖)

ribbon-3.png

  1. 写注解

    // 如果使用RestTemplate整合Ribbon,则需为RestTemplate添加注解
    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
      return new RestTemplate();
    }
    复制代码
  2. 写配置(没有配置)

Ribbon组成

接口 作用 默认值
IClientConfig 读取配置 DefaultClientConfigImpl
IRule 负载均衡规则,选择实例 ZoneAvoidanceRule
IPing 筛选掉ping不通的实例 DummyPing
ServerList 交给Ribbon的实例列表 Ribbon:ConfigurantionBasedServerList. Spring Cloud Alibaba:NacosServerList
ServerListFilter 过滤掉不符合条件的实例 ZonePreferenceServerListFilter
ILoadBalancer Ribbon的入口 ZoneAwareLoadBalancer
ServerListUpdater 更新交给Ribbon的List的策略 PollingServerListUpdater

我们可以实现这些接口,制定我们自定义的需求。

Ribbon内置的负载均衡规则

规则名称 特点
AvailabilityFilterRule 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就是检查status里面记录的各个Server的运行状态。
BestAvailableRule 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。
RandomRule 随机选择一个Server。
ResponseTimeWeightedRule 已废弃。作用同WeightedResponseTimeRule。
RetryRule 对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server。
RoundRobinRule 轮询选择,轮询index,选择index对应位置的Server。
WeightedResponseTimeRule 根据相应时间加权,响应时间越长,权重越小,被选中的概率越低。
ZoneAvoidanceRule 默认的规则。复合判断Server所Zone的性能和Server的可用性选择Server,在没有Zone的环境下,类似于轮询(RoundRobinRule)

细粒度配置

Java代码方式

ribbon-4.png

​ 注意:RibbonConfiguration必须写在Springboot包扫描外。(父子上下文解释)

  1. 为某个服务创建Ribbon配置类

    package com.samir.contentcenter.configuration;
    
    import org.springframework.cloud.netflix.ribbon.RibbonClient;
    import org.springframework.context.annotation.Configuration;
    import ribbonconfiguration.RibbonConfiguration;
    
    @Configuration
    @RibbonClient(name = "user-center", configuration = RibbonConfiguration.class)
    public class UserCenterRibbonConfiguration {
    }
    复制代码
  2. 创建当前服务使用的负载均衡规则

    package ribbonconfiguration;
    
    import com.netflix.loadbalancer.IRule;
    import com.netflix.loadbalancer.RandomRule;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RibbonConfiguration {
        @Bean
        public IRule ribbonRule() {
            return new RandomRule();
        }
    }
    复制代码

父子上下文

  1. @Configuration注解也是一种特殊的Component注解,而在启动类上面的@SpringBootApplication是一个聚合注解,其包含了@ComponentScan注解。

  2. 所有的Component注解都会被扫描到(当前启动类所在包及子包)。

  3. @SpringBootApplication扫描到的Component注解为父上下文。

  4. Ribbon也会扫描到Component注解,为子上下文。

  5. 负载均衡规则的包只能放在外面。如果父子上下文重叠会导致事务不生效等一系列奇葩问题

    Ribbon官方解释:客户端配置负载均衡规则必须使用@Configuration注解,但是请注意不能被@ComponentScan这个主上下文扫描到。否则,它将被所有的@RibbonClients共享。变成了全局的配置。

配置属性方式

​ .ribbon.NFLoadBalancerRuleClassName = 规则的全路径

# 服务在注册中心的名称
user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 规则的全路径
复制代码

两种方式对比

配置方式 优点 缺点
代码配置 基于代码,更加灵活 注意父子上下文的问题;线上修改需要重新打包发布
属性配置 易上手;配置简洁直观;线上修改无需重新打包发布(配置配置中心);优先级更高 极端场景下没有代码配置方式灵活

最佳实现

  • 尽量使用属性配置,属性方式实现不了的时候再考虑代码配置。
  • 在同一个微服务中尽量保持单一性,不要两种方式混用,增加定位代码的复杂性。简单就是美

全局配置

  • 方式一:让ComponentSacn父子上下文重叠(强烈不建议使用)。(见父子上下文详解或者官方文档)

  • 方式二:@RibbonClients(defaultConfiguration=xxx.class)

    package com.samir.contentcenter.configuration;
    
    import org.springframework.cloud.netflix.ribbon.RibbonClients;
    import org.springframework.context.annotation.Configuration;
    import ribbonconfiguration.RibbonConfiguration;
    
    @Configuration
    @RibbonClients(defaultConfiguration = RibbonConfiguration.class) // 注意这里
    public class UserCenterRibbonConfiguration {
    }
    复制代码

支持的配置项

代码方式

  1. 为某个服务创建Ribbon配置类

    package com.samir.contentcenter.configuration;
    
    import org.springframework.cloud.netflix.ribbon.RibbonClient;
    import org.springframework.context.annotation.Configuration;
    import ribbonconfiguration.RibbonConfiguration;
    
    @Configuration
    @RibbonClient(name = "user-center", configuration = RibbonConfiguration.class)
    public class UserCenterRibbonConfiguration {
    }
    复制代码
  2. 创建当前服务使用的负载均衡规则

    package ribbonconfiguration;
    
    import com.netflix.loadbalancer.IRule;
    import com.netflix.loadbalancer.RandomRule;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RibbonConfiguration {
        @Bean
        public IRule ribbonRule() {
            return new RandomRule();
        }
      
      	@Bean
        public IPing ping() {
            return new PingUrl();
        }
      
      	// 这里从Ribbon的组成去找对应的接口,再这里实现就好了
    }
    复制代码

配置属性方式

使用 **.ribbon.属性 ** 的方式。如下属性:

  • NFLoadBalancerRuleClassName: ILoadBalancer的实现类
  • NFLoadBalancerRuleClassClassName: IRule的实现类
  • NFLoadBalancerPingClassClassName: IPing的实现类
  • NIWSServerListClassName: ServerList的实现类
  • NIWSServerListFilterClassName: ServerListFilter的实现类

饥饿加载

会导致第一次请求很慢的问题,可以通过以下方案解决

# ribbon开启饥饿加载(默认是关闭的)
ribbon:
  eager-load:
    enabled: true
    clients: user-center # 配置为哪些服务开启饥饿加载(多个以,分割)
复制代码

扩展Ribbon

支持nacos权重

  1. 在nacos控制台可以为每个实例配置权重信息。如下图:

ribbon-5.png

阀值值越大,被请求的概率大一些;而阀值的使用在分布式场景中很实用,因为我们的服务器性能是不一致的,我们可以通过阀值的设置让性能好的服务器处更多的请求。

  1. Ribbon内置的负载均衡规则不支持阀值的方式,故我们需要自己根据nacos来扩展(两种实现方式:实现IRule接口、继承AbstractLoadBalancerRule类)。下面以配置属性的方式局部配置。

    1. 编辑自定义实现负载均衡规则

      package com.samir.contentcenter.configuration;
      
      import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
      import com.alibaba.cloud.nacos.ribbon.NacosServer;
      import com.alibaba.nacos.api.exception.NacosException;
      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;
      
      @Slf4j
      public class NacosWeightedRule  extends AbstractLoadBalancerRule {
          @Autowired
          private NacosDiscoveryProperties nacosDiscoveryProperties;
      
          @Override
          public void initWithNiwsConfig(IClientConfig iClientConfig) {
              // 读取配置文件并初始化,用不上,留空
          }
      
          @Override
          public Server choose(Object o) {
              try {
                  // ribbon的入口
                  BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
                  // 获取想要请求的微服务的名称
                  String name = loadBalancer.getName();
                  // 拿到服务发现的相关api
                  NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
                  // nacos client 自动通过基于权重的负载均衡算法,给我选择一个实例
                  Instance instance = namingService.selectOneHealthyInstance(name);
      
                  log.info("选择的实例是: port = {}, instance = {}", instance.getPort(), instance);
                  return new NacosServer(instance);
              } catch (NacosException e) {
                  e.printStackTrace();
                  return null;
              }
          }
      }
      复制代码
    2. 编辑配置文件

      # 服务在注册中心的名称
      user-center:
        ribbon:
          NFLoadBalancerRuleClassName: com.samir.contentcenter.configuration.NacosWeightedRule # 规则的全路径
      复制代码

同一集群优先加载

  1. 编辑自定义实现负载均衡规则

    package com.samir.contentcenter.configuration;
    
    import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
    import com.alibaba.cloud.nacos.ribbon.NacosServer;
    import com.alibaba.nacos.api.exception.NacosException;
    import com.alibaba.nacos.api.naming.NamingService;
    import com.alibaba.nacos.api.naming.pojo.Instance;
    import com.alibaba.nacos.client.naming.core.Balancer;
    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;
    import org.springframework.util.CollectionUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    @Slf4j
    public class NacosSameClusterWeigthedRule extends AbstractLoadBalancerRule {
    
        @Autowired
        private NacosDiscoveryProperties nacosDiscoveryProperties;
    
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
    
        }
    
        @Override
        public Server choose(Object o) {
            try {
                // 获取当前应用(配置文件中   spring.cloud.nacos.discovery.cluster-name: BJ )的集群名称
                String clusterName = nacosDiscoveryProperties.getClusterName();
                BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
                // 获取想要请求的微服务名称
                String name = loadBalancer.getName();
                // 拿到服务发现的相关api
                NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
    
                // 1.找到指定服务的所有实例 A
                List<Instance> instances = namingService.selectInstances(name, true);// true只获取健康的实例
                // 2.过滤出相同集群下的所有实例 B
                List<Instance> instanceList = instances.stream().filter(instance -> Objects.equals(instance.getClusterName(), clusterName)).collect(Collectors.toList());
                // 3.如果B为空,则使用A
                List<Instance> instancesToBeChosen = new ArrayList<>();
                if (CollectionUtils.isEmpty(instanceList)) {
                    instancesToBeChosen = instances;
                    log.warn("发生跨集群的调用,name = {}, clusterName = {}, instances = {}", name, clusterName, instancesToBeChosen);
                } else {
                    instancesToBeChosen = instanceList;
                }
                // 4.基于权重非负载均衡算法,返回一个实例
                Instance instance = MyBalancer.myGetHostByRandomWeight(instancesToBeChosen);
                return new NacosServer(instance);
            } catch (NacosException e) {
                log.error("发生异常了", e);
                return null;
            }
        }
    }
    
    /**
     * 这个方法是继承Balancer类来使用他的protected类型的方法
     *
     * 通过namingService.selectOneHealthyInstance方法的跟踪,找到下面的方法
     *      public Instance selectOneHealthyInstance(String serviceName, String groupName, List<String> clusters, boolean subscribe) throws NacosException {
     *         return subscribe ? RandomByWeight.selectHost(this.hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","))) : RandomByWeight.selectHost(this.hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ",")));
     *     }
     *
     *     查看selectHost方法
     *     public static Instance selectHost(ServiceInfo dom) {
     *             List<Instance> hosts = selectAll(dom);
     *             if (CollectionUtils.isEmpty(hosts)) {
     *                 throw new IllegalStateException("no host to srv for service: " + dom.getName());
     *             } else {
     *                 return Balancer.getHostByRandomWeight(hosts); // 这个静态方法就是实现的关键
     *             }
     *         }
     */
    class MyBalancer extends Balancer {
        public static Instance myGetHostByRandomWeight(List<Instance> hosts) {
            return getHostByRandomWeight(hosts);
        }
    }
    复制代码
  2. 编辑配置文件

    # 服务在注册中心的名称
    user-center:
      ribbon:
        NFLoadBalancerRuleClassName: com.samir.contentcenter.configuration.NacosSameClusterWeigthedRule # 规则的全路径
    复制代码

基于元数据的版本控制

  1. 编辑自定义实现负载均衡规则

    @Slf4j
    public class NacosFinalRule extends AbstractLoadBalancerRule {
        @Autowired
        private NacosDiscoveryProperties nacosDiscoveryProperties;
    
        @Override
        public Server choose(Object key) {
            // 负载均衡规则:优先选择同集群下,符合metadata的实例
            // 如果没有,就选择所有集群下,符合metadata的实例
    
            // 1. 查询所有实例 A
            // 2. 筛选元数据匹配的实例 B
            // 3. 筛选出同cluster下元数据匹配的实例 C
            // 4. 如果C为空,就用B
            // 5. 随机选择实例
          
          	// 元数据信息 (配置文件中   spring.cloud.nacos.metadata.version: v1)自己这里应用的版本
          	// 元数据信息 (配置文件中   spring.cloud.nacos.metadata.target-version: v1)允许调用的提供者的版本
            try {
                String clusterName = this.nacosDiscoveryProperties.getClusterName();
                String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");
    
                DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
                String name = loadBalancer.getName();
    
                NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
    
                // 所有实例
                List<Instance> instances = namingService.selectInstances(name, true);
    
                List<Instance> metadataMatchInstances = instances;
                // 如果配置了版本映射,那么只调用元数据匹配的实例
                if (StringUtils.isNotBlank(targetVersion)) {
                    metadataMatchInstances = instances.stream()
                            .filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
                            .collect(Collectors.toList());
                    if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                        log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}", targetVersion, instances);
                        return null;
                    }
                }
    
                List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
                // 如果配置了集群名称,需筛选同集群下元数据匹配的实例
                if (StringUtils.isNotBlank(clusterName)) {
                    clusterMetadataMatchInstances = metadataMatchInstances.stream()
                            .filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
                            .collect(Collectors.toList());
                    if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                        clusterMetadataMatchInstances = metadataMatchInstances;
                        log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
                    }
                }
    
                Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
                return new NacosServer(instance);
            } catch (Exception e) {
                log.warn("发生异常", e);
                return null;
            }
        }
    
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
        }
    }
    
    class ExtendBalancer extends Balancer {
        /**
         * 根据权重,随机选择实例
         *
         * @param instances 实例列表
         * @return 选择的实例
         */
        public static Instance getHostByRandomWeight2(List<Instance> instances) {
            return getHostByRandomWeight(instances);
        }
    }
    复制代码
  2. 编辑配置文件

    # 服务在注册中心的名称
    user-center:
      ribbon:
        NFLoadBalancerRuleClassName: com.samir.contentcenter.configuration.NacosFinalRule # 规则的全路径
    复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享