Spring Cloud Ribbon负载均衡器各项配置和部分加载源码原理分析

前言

  • Spring Cloud Ribbon是基于Netflix Ribbon 实现的一套客户端的负载均衡工具,Ribbon客户端组件提
    供一系列的完善的配置,如超时,重试等。通过Load Balancer获取到服务提供的所有机器实例,
    Ribbon会自动基于某种规则(轮询,随机)去调用这些服务,Ribbon也可以实现我们自己的负载均衡算
    法,这里会使用Nacos作为注册中心。
  • 本文会讲解Ribbon的各项常用配置和扩展点,也会对部分配置深入源码讲解,在实际项目中使用Ribbon还是有一些坑的。

一、前置准备

前置生产者接口准备(后续都会用到)

这里我们准备两个生产者服务user-app,goods-app每个服务提供一个接口用来测试负载均衡效果。

user-app(启动两个实例)

端口:8310、8311

@RestController
@RequestMapping("/user-info")
public class UserInfoCtoller {
    
    
    @GetMapping("/info/{id}")
    public String getInfo(@PathVariable("id") Long id) {
    
    
        return "用户-"+id;
    }
}

goods-app(启动一个实例)

端口:8320

@RestController
@RequestMapping("/goods-info")
public class UserInfoCtoller {
    
    
    @GetMapping("/info/{id}")
    public String getInfo(@PathVariable("id") Long id) {
    
    
        return "商品-"+id;
    }
}

消费者使用RestTemplate集成Ribbon测试接口调用

使用RestTemplate和OpenFeign集成Ribbon使用上原理差不多,如果使用OpenFeign来研究Ribbon干扰项太多,这里使用RestTemplate会比较清晰。

POM

包版本自己根据自己项目来就行,我这里使用的spring-boot-starter-parent版本是2.3.12.RELEASE,spring-cloud-dependencies版本是Hoxton.SR12,spring-cloud-alibaba-dependencies版本是2.2.9.RELEASE。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--    nacos注册中心包中已经包含了ribbon的包不需要单独引入    -->
<!--        <dependency>-->
<!--            <groupId>org.springframework.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>-->
<!--        </dependency>-->

配置RestTemplate使用Ribbon负载均衡器

在SpringCloud项目中需要自己定义一个RestTemplate的Bean,使用@LoadBalanced标记就能使用Ribbon来做负载均衡了,在org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration中会去做加载,想了解加载过程的可以看看这篇文章《SpringCloud中RestTemplate集成Ribbon源码分析

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
    
    
        return new RestTemplate();
    }

测试接口调用

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/m1")
    public String m1(){
    
    
        String url = "http://user-app/user-info/info/666";  
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
// 处理响应数据
        System.out.println("Status code-------------------: " + response.getStatusCodeValue());
        System.out.println("Response body----------------------: " + response.getBody());
        return response.getBody();
    }

二、配置讲解(部分配置会对源码进行分析)

2.1 基础配置(超时、重试配置)

注意:在RestTemplate集成Ribbon的时候要想实现超时重试功能只配置Ribbon的全局配置文件是无效的,必须要添加spring-retry包,还要对RestTemplate单独设置链接超时和请求超时时间,RestTemplate是不会用到Ribbon的超时全局配置的。

  • 1、POM
        <dependency>
            <artifactId>spring-retry</artifactId>
            <groupId>org.springframework.retry</groupId>
        </dependency>
  • 2、配置RestTemplate
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
    
    
        SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
        // 设置连接超时时间为1秒 默认不超时会一直等待
        simpleClientHttpRequestFactory.setConnectTimeout(1000);
        // 设置读取超时时间为2秒 默认不超时会一直等待
        simpleClientHttpRequestFactory.setReadTimeout(2000);
        RestTemplate restTemplate =new RestTemplate(simpleClientHttpRequestFactory);
        return restTemplate;
    }
  • 3、Ribbon全局参数配置
ribbon:
  # ConnectTimeout: 1000 #链接超时 RestTemplate是不会使用这个配置的
  # ReadTimeout: 1000 #读取超时  RestTemplate是不会使用这个配置的
  MaxAutoRetries: 0 #同一台实例最大重试次数,不包括首次调用 默认0
  MaxAutoRetriesNextServer: 1 #服务实例切换重试次数 默认1
  OkToRetryOnAllOperations: true #是否开启重试机制 默认false  需要注意一点 只有GET请求才会重试
  retryableStatusCodes: 200,500,503 #需要重试的响应状态 这里设置一个200方便测试,如果不配置请求服务实例报错的时候也会重试
核心源码
加载流程

1、在RibbonAutoConfiguration 配置类中会判断系统是否有引入RetryTemplate 类,这个类是在spring-retry 包里的,如果引入了就会去加载一个LoadBalancedRetryFactory 到IOC中,在后续的LoadBalancerAutoConfiguration 中会需要使用到这个对象。
在这里插入图片描述
2、在LoadBalancerAutoConfiguration 中也会有个类似的处理如果引入RetryTemplate 类就会去加载重试负载均衡拦截器配置类RetryInterceptorAutoConfiguration,不会加载普通的LoadBalancerInterceptorConfig,如果没有引入RetryTemplate 类就会去加载LoadBalancerInterceptorConfig,这个类中加载出的RetryLoadBalancerInterceptor是具有重试功能的拦截器。
在这里插入图片描述
没有引入RetryTemplate 类则会加载LoadBalancerInterceptorConfig 没有重试功能。
在这里插入图片描述

配置项使用源码定位
  • 1、通过SimpleClientHttpRequestFactory设置的RestTemplate连接超时和请求超时

    • RestTemplatedoExecute方法中可以看到一个createRequest操作,会调用到父抽象类HttpAccessorcreateRequest方法,这里会获取到一个requestFactory就是我们在创建RestTemplate的时候设置的SimpleClientHttpRequestFactory对象,通过这个对象来创建的ClientHttpRequest对象具体实现类用的是InterceptingClientHttpRequest
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 2、通过ribbon全局配置设置的重试参数最大重试次数、服务实例切换重试次数、是否开启重试机制开关

    • 重试信息是在RibbonClientConfiguration配置类中加载的, 通过retryHandler方法注册了一个DefaultLoadBalancerRetryHandler然后在ribbonLoadBalancerContext方法中会将这个RetryHandler设置到RibbonLoadBalancerContext中去,在后续流程中就会通过RibbonLoadBalancerContext这个对象取重试次数和服务开启重试配置
      在这里插入图片描述
      在这里插入图片描述
  • 3、需要重试的响应状态

    • 这个响应状态会在的RibbonLoadBalancedRetryFactorycreateRetryPolicy方法中创建一个RibbonLoadBalancedRetryPolicy对象,并且在构造函数中读取并且加载到retryableStatusCodes集合中,在请求响应后会对这个状态列表进行判断如果包含就会进行重试。
      在这里插入图片描述
      在这里插入图片描述

2.2 饥饿加载配置

ribbon:
  eager‐load:
  # 开启ribbon饥饿加载 默认false
    enabled: true
    # 配置使用ribbon饥饿加载的服务名称,多个使用逗号分隔
    clients: user‐app,goods-app
核心源码

饥饿加载配置会在RibbonAutoConfiguration自动配置类中通过@ConditionalOnProperty(“ribbon.eager-load.enabled”)的方式加载,会根据 ribbon.eager‐load.clients 中配置的服务名称在项目启动时就去加载。
在这里插入图片描述

2.3 刷新服务列表间隔时间

ribbon:
  ServerListRefreshInterval: 5000 #刷新所服务列表间隔时间 默认30s
核心源码
  • 这个刷新服务列表时间间隔的配置也是在RibbonClientConfiguration配置类中进行的注册,通过ribbonServerListUpdater方法注册一个PollingServerListUpdater对象来进行服务列表管理,这个PollingServerListUpdaterstart方法就是定时刷新服务列表的核心方法,会在DynamicServerListLoadBalancerrestOfInit方法中调用
    在这里插入图片描述
    在这里插入图片描述

2.4 负载均衡Rule配置

通过全局配置(这种方法有问题)

PS:这里有个大坑,通过这种全局配置负载均衡器根本不会生效,需要自己用配置类配置或者单独配置每个服务的Rule,使用配置类配置也有大坑会出现所有服务用的都是同一个ILoadBalancer实例,在Ribbon中应该是每个服务都有一个自己的全部实例包括ILoadBalancer、IRule、IClientConfig、ServerListUpdater等,必须要单独配置每个服务的Rule

ribbon:
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置随机负载均衡器,其他ribbon自带的规则 可查看IRule接口的实现类

原因分析:
这个配置的确会被读取到IClientConfig中,但是每个服务在加载自己的IRule时会调用一个this.propertiesFactory.isSet(IRule.class, name),会去获取每个服务的单独配置,如果没设置单独配置,那么就会使用ZoneAvoidanceRule作为负载均衡器,propertiesFactory个isSet方法中调用了一个getClassName,在获取className的时候是获取的单独服务设置(name + “.” + NAMESPACE + “.” + classNameProperty)-> (user-app.ribbon.NFLoadBalancerRuleClassName)
在这里插入图片描述
在这里插入图片描述

通过局部配置
user-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

三、扩展点

3.1 订阅Nacos实现实时刷新服务列表

  • 要想实现订阅Nacos实现实时刷新服务列表解决两个问题就能实现:
    • 1、如何订阅Nacos获取上线下线的服务实例信息
    • 2、如何去刷新Ribbon中的服务列表

这里直接上实现代码了,代码实现比较简单但是原理涉及内容比较多我这里也不会过多描述,订阅Nacos获取上线下线的服务实例信息这个网上很好找到,重点在Ribbon这边,如果对Ribbon在SpringCloud中如何加载不是很清楚就很难办,这里简单说一下,在SpringCloud中通过starter包集成Ribbon的话都会加载一个SpringClientFactory,这个工厂类是最为核心的一个类,每一个服务都会创建一个自己的Spring上下文独立于主Spring上下文之外的也就是说在主的IOC根本就找不到,而每个服务的Spring上下文都是由SpringClientFactory的getInstance方法创建的,SpringClientFactory集成了NamedContextFactory会将创建的每一个服务的Spring上下文存储到contexts属性中,通过SpringClientFactory的getInstance就能获取到对应服务的Spring上下文,这个上下文中就包含了一个服务所有的Ribbon组件,比如:ILoadBalancer、IRule、IClientConfig、ServerListUpdater等,每个服务都是独立的,比如user-app有user-app的Spring上下文goods-app也有自己独立的上下文所以都有自己独立的Ribbon组件,但是不能在Spring主上下文中自己去注册这些组件,不然在加载每个服务的Spring上下文是会获取到父容器的bean,这里涉及东西比较多有兴趣可以自己去阅读一下源码,阅读前提是有Spring和SpringBoot源码基础不然看起来比较吃力。

@Slf4j
@Component
public class NacosServerStatusListener implements SmartInitializingSingleton {
    
    
    // 该对象会在RibbonAutoConfiguration中被加载,可以通过这个工厂获取对应服务负载均衡器,从而刷新指定rebbon中的服务地址信息
    @Autowired
    private SpringClientFactory springClientFactory;

    // Nacos注册中心配置信息 包括我们需要的NamingService也能在里面获取到
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    // 需要订阅的服务名称
    private List<String> serviceList = Arrays.asList("user-app", "goods-app");

    @Override
    public void afterSingletonsInstantiated() {
    
    
        try {
    
    
            //获取 NamingService
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
            //订阅服务,服务状态刷新时,更新ribbon
            serviceList.stream().forEach(service -> {
    
    
                //订阅服务状态发生改变时,刷新 ribbon 服务实例
                try {
    
    
                    namingService.subscribe(service, (event -> {
    
    
                    	log.info("刷新 ribbon 服务实例:{}", service);
                        ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer(service);
                        if (loadBalancer != null) {
    
    
                            ((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers();
                            log.info("刷新 ribbon 服务实例成功:{}", service);
                        }
                    }));
                } catch (NacosException e) {
    
    
                    log.error("订阅 nacos 服务失败,error:{}", e.getErrMsg());
                    e.printStackTrace();
                }
            });
        } catch (Exception e) {
    
    
            log.error("获取 nacos 服务信息失败,error:{}", e.getMessage());
            e.printStackTrace();
        }
    }
}

3.2 自定义负载均衡器

  • 1、自定义随机负载均衡器代码实现
public class MyRandomRule extends AbstractLoadBalancerRule {
    
    
    @Override
    public Server choose(Object key) {
    
    
        ILoadBalancer loadBalancer = getLoadBalancer();
        if (loadBalancer == null) {
    
    
            return null;
        }
        List<Server> allServers = loadBalancer.getAllServers();
        if(allServers == null || allServers.size() == 0){
    
    
            return null;
        }
        Server server = null;
        int index = 0;
        if(allServers.size() > 1){
    
    
            Random random =new Random();
            index =  random.nextInt(allServers.size());
        }
        server = allServers.get(index);
        System.out.println("server = "+server.toString());

        return server;
    }
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    
    

    }
}
  • 2、配置user-app服务使用自定义负载均衡器(只能独立配置,不能使用全局配置不然会出问题)
user-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.kerwin.order.ribbon.config.MyRandomRule

3.3 通过Ribbon实现灰度发布

要使用Ribbon实现灰度发布有两种方式,第一种是通过ServerListFilter实现,第二种是通过负载均衡器IRule实现,核心思想都是借助于Nacos的Metadata实现,在Metadata中存放一个version值用来判断调用什么版本的服务,这里会通过ServerListFilter做简单实现可以根据自己的需要扩展,通过IRule实现原理也是类似的只是时机不同,ServerListFilter是在拉去配置信息时就会过滤服务列表而IRule是在发起RPC请求时才会判断,根据自己实际业务选择方案即可。

  • 1、代码实现
public class MyServerListFilter implements ServerListFilter<NacosServer> {
    
    
    // 当前发布版本
    private String version="V2";
    @Override
    public List<NacosServer> getFilteredListOfServers(List<NacosServer> servers) {
    
    
        if(servers == null || servers.size() == 0){
    
    
            return null;
        }
        List<NacosServer> nacosServers = new ArrayList<>();
        for (NacosServer server : servers) {
    
    
            Map<String, String> metadata = server.getMetadata();
            String metadataVersion = metadata.get("version");
            if(metadataVersion != null && version.equals(metadataVersion)){
    
    
                nacosServers.add(server);
            }
        }
        return nacosServers;
    }
}
  • 2、配置调用user-app服务使用自定义拦截器

    • 生产者user-app 服务配置Nacos metadata
    spring:
      application:
        name: user-app
      cloud:
        nacos:
          discovery:
            namespace: springcloud-ribbon-example
            metadata:
              version: V2
          server-addr: 119.91.223.226:8848
    
    • 消费者配置自定义拦截器(只能独立配置,不能使用全局配置不然会出问题)
    	user-app:
    	  ribbon:
    	    NIWSServerListFilterClassName: com.kerwin.order.ribbon.config.MyServerListFilter
    

猜你喜欢

转载自blog.csdn.net/weixin_44606481/article/details/131619483