【ribbon】Ribbon的负载均衡和扩展功能

Ribbon的核心接口

参考:org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration

  • IClientConfig:Ribbon的客户端配置,默认采用DefaultClientConfigImpl实现。
  • IRule:Ribbon的负载均衡策略,默认采用ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。
  • IPing:Ribbon的实例检查策略,默认采用DummyPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的。
  • ServerList:服务实例清单的维护机制,默认采用ConfigurationBasedServerList实现。
  • ServerListFilter:服务实例清单过滤机制,默认采ZonePreferenceServerListFilter,该策略能够优先过滤出与请求方处于同区域的服务实例。
  • ILoadBalancer:负载均衡器,默认采用ZoneAwareLoadBalancer实现,它具备了区域感知的能力。

Ribbon负载均衡策略

【ribbon】Ribbon的负载均衡和扩展功能_第1张图片

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

修改默认负载均衡策略

全局配置

全局配置:所有调用的微服务一律使用指定的负载均衡策略,只需要向容器中注入IRule实例即可。

package com.morris.user.config;

import com.alibaba.cloud.nacos.ribbon.NacosRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfig {
    @Bean
    public IRule ribbonRule() {
        // 指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机权重)
        return new NacosRule();
    }
}

局部配置

局部配置:可以在配置文件中调用指定微服务时,使用对应的负载均衡策略。

# 被调用的微服务名
order-service:
  ribbon:
    # 指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机&权重)
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

自定义负载均衡策略

通过实现IRule接口可以自定义负载策略,主要的选择服务逻辑在choose方法中。

实现基于Nacos权重的负载均衡策略:

package com.morris.user.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.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.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Resource;

@Slf4j
public class NacosRandomWithWeightRule extends AbstractLoadBalancerRule {


    @Resource
    private NacosServiceManager nacosServiceManager;

    @Resource
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public Server choose(Object key) {
        DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
        String serviceName = loadBalancer.getName();
        NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());
        try {
            //nacos基于权重的算法
            Instance instance = namingService.selectOneHealthyInstance(serviceName);
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("获取服务实例异常:{}", e.getMessage());
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }
}

可以将NacosRandomWithWeightRule按照上面的全局配置或者局部配置。

饥饿加载

在进行服务调用的时候,如果网络情况不好,第一次调用会超时。Ribbon默认懒加载,意味着只有在发起调用的时候才会创建客户端。

应用启动后第一次请求会有2s左右的延时,如果此时有大量请求进来就会抛出大量异常。

2023-07-21 16:50:37.718  INFO 20488 --- [nio-8030-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-07-21 16:50:37.718  INFO 20488 --- [nio-8030-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-07-21 16:50:37.733  INFO 20488 --- [nio-8030-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 15 ms
2023-07-21 16:50:39.332  INFO 20488 --- [nio-8030-exec-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client: order-service instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=order-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2023-07-21 16:50:39.346  INFO 20488 --- [nio-8030-exec-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2023-07-21 16:50:39.534  INFO 20488 --- [nio-8030-exec-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client order-service initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=order-service,current list of Servers=[2.0.0.1:8020],Load balancer stats=Zone stats: {unknown=[Zone:unknown;	Instance count:1;	Active connections count: 0;	Circuit breaker tripped count: 0;	Active connections per server: 0.0;]
},Server stats: [[Server:2.0.0.1:8020;	Zone:UNKNOWN;	Total Requests:0;	Successive connection failure:0;	Total blackout seconds:0;	Last connection made:Thu Jan 01 08:00:00 CST 1970;	First connection made: Thu Jan 01 08:00:00 CST 1970;	Active Connections:0;	total failure count in last (1000) msecs:0;	average resp time:0.0;	90 percentile resp time:0.0;	95 percentile resp time:0.0;	min resp time:0.0;	max resp time:0.0;	stddev resp time:0.0]
]}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@fb090e

开启饥饿加载,解决第一次调用慢的问题:

ribbon:
  eager-load:
    enabled: true
    clients: order-service

参数说明:

  • ribbon.eager-load.enabled:开启ribbon的饥饿加载模式
  • ribbon.eager-load.clients:指定需要饥饿加载的服务名,也就是你需要调用的服务,如果有多个服务,则用逗号隔开

看下效果,在启动过程中就已经初始化连接了:

2023-07-21 16:54:18.480  INFO 22040 --- [           main] com.morris.user.UserServiceApplication   : Started UserServiceApplication in 6.846 seconds (JVM running for 8.265)
2023-07-21 16:54:19.254  INFO 22040 --- [           main] c.netflix.loadbalancer.BaseLoadBalancer  : Client: order-service instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=order-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2023-07-21 16:54:19.260  INFO 22040 --- [           main] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2023-07-21 16:54:19.286  INFO 22040 --- [           main] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client order-service initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=order-service,current list of Servers=[2.0.0.1:8020],Load balancer stats=Zone stats: {unknown=[Zone:unknown;	Instance count:1;	Active connections count: 0;	Circuit breaker tripped count: 0;	Active connections per server: 0.0;]
},Server stats: [[Server:2.0.0.1:8020;	Zone:UNKNOWN;	Total Requests:0;	Successive connection failure:0;	Total blackout seconds:0;	Last connection made:Thu Jan 01 08:00:00 CST 1970;	First connection made: Thu Jan 01 08:00:00 CST 1970;	Active Connections:0;	total failure count in last (1000) msecs:0;	average resp time:0.0;	90 percentile resp time:0.0;	95 percentile resp time:0.0;	min resp time:0.0;	max resp time:0.0;	stddev resp time:0.0]
]}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@432469

NacosRule源码分析

com.alibaba.cloud.nacos.ribbon.NacosRule#choose

@Override
public Server choose(Object key) {
    try {
        String clusterName = this.nacosDiscoveryProperties.getClusterName();
        String group = this.nacosDiscoveryProperties.getGroup();
        DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
        String name = loadBalancer.getName();

        // 获取NamingService,在我们的代码中也可以这么使用
        NamingService namingService = nacosServiceManager
                .getNamingService(nacosDiscoveryProperties.getNacosProperties());
        // 筛选出同一个group的实例,不同group之间不能通讯
        List<Instance> instances = namingService.selectInstances(name, group, true);
        if (CollectionUtils.isEmpty(instances)) {
            LOGGER.warn("no instance in service {}", name);
            return null;
        }

        List<Instance> instancesToChoose = instances;
        if (StringUtils.isNotBlank(clusterName)) {
            // 找出同一个集群的节点
            List<Instance> sameClusterInstances = instances.stream()
                    .filter(instance -> Objects.equals(clusterName,
                            instance.getClusterName()))
                    .collect(Collectors.toList());
            if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                instancesToChoose = sameClusterInstances;
            }
            else {
                LOGGER.warn(
                        "A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
                        name, clusterName, instances);
            }
        }

        // 带权重的随机选择一个节点
        Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);

        return new NacosServer(instance);
    }
    catch (Exception e) {
        LOGGER.warn("NacosRule error", e);
        return null;
    }
}

NacosRule是AlibabaNacos自己实现的一个负载均衡策略,可以在nacos平台中根据自定义权重进行访问。

基于Nacos元数据的版本控制自定义负载均衡

实际项目,我们可能还会有这样的需求:

一个微服务在线上可能多版本共存,且多个版本的微服务并不兼容。使用Nacos的自定义元数据,可以实现微服务的版本控制。

配置文件的格式: spring.cloud.nacos.discovery.metadata.{key}={value}

当前配置的版本: spring.cloud.nacos.discovery.metadata.version=V1

package com.morris.user.config;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.ribbon.ExtendBalancer;
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.apache.commons.lang.StringUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
public class ClusterMetaDataRibbonRule extends AbstractLoadBalancerRule {
 
    @Resource
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Resource
    private NacosServiceManager nacosServiceManager;
 
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }
 
    @Override
    public Server choose(Object o) {
        log.info("-------key: {}", o);
 
        // 获取当前服务的集群名称
        String currentClusterName = nacosDiscoveryProperties.getClusterName();
 
        // 获取当前版本
        String currentVersion = nacosDiscoveryProperties.getMetadata().get("version");
 
        // 获取被调用的服务的名称
        BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) getLoadBalancer();
        String serviceName = baseLoadBalancer.getName();
 
        // 获取nacos clinet的服务注册发现组件的api
        NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());
 
        try {
            // 获取所有被调用服务
            List<Instance> allInstances = namingService.getAllInstances(serviceName);
 
            // 过滤出相同版本且相同集群下的所有服务
            List<Instance> sameVersionAndClusterInstances = allInstances.stream()
                    .filter(x -> StringUtils.equalsIgnoreCase(x.getMetadata().get("version"), currentVersion)
                            && StringUtils.equalsIgnoreCase(x.getClusterName(), currentClusterName)
                    ).collect(Collectors.toList());
 
            Instance chooseInstance;
            if(sameVersionAndClusterInstances.isEmpty()) {
                // 过滤出所有相同版本的服务
                List<Instance> sameVersionInstances = allInstances.stream()
                        .filter(x -> StringUtils.equalsIgnoreCase(x.getMetadata().get("version"), currentVersion))
                        .collect(Collectors.toList());
                if(sameVersionInstances.isEmpty()) {
                    log.info("跨集群调用找不到对应合适的版本当前版本为:currentVersion:{}",currentVersion);
                    throw new RuntimeException("找不到相同版本的微服务实例");
                }
                else {
                    // 随机权重
                    chooseInstance = ExtendBalancer.getHostByRandomWeight2(sameVersionInstances);
                    log.info("跨集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
                            currentClusterName, chooseInstance.getClusterName(), chooseInstance.getMetadata().get("current-version"),
                            chooseInstance.getMetadata().get("current-version"), chooseInstance.getIp(), chooseInstance.getPort());
                }
            }
            else {
                chooseInstance = ExtendBalancer.getHostByRandomWeight2(sameVersionAndClusterInstances);
                log.info("同集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
                        currentClusterName, chooseInstance.getClusterName(), chooseInstance.getMetadata().get("version"),
                        chooseInstance.getMetadata().get("current-version"), chooseInstance.getIp(), chooseInstance.getPort());
            }
            return new NacosServer(chooseInstance);
        } catch (NacosException e) {
            log.error("error,", e);
            return null;
        }
    }
}

你可能感兴趣的:(springcloud,ribbon,负载均衡,java,spring,boot,springcloud)