Analysis of various configurations of Spring Cloud Ribbon load balancer and principle of partial loading source code

foreword

  • Spring Cloud Ribbon is a set of client load balancing tools based on Netflix Ribbon. The Ribbon client component provides
    a series of complete configurations, such as timeout and retry. Obtain all machine instances provided by the service through Load Balancer, and
    Ribbon will automatically call these services based on certain rules (polling, random). Ribbon can also implement our own load balancing algorithm
    , and Nacos will be used as the registration center here.
  • This article will explain the common configurations and extension points of Ribbon, and will also explain some configurations in depth. There are still some pitfalls in using Ribbon in actual projects.

1. Preparation

Pre-producer interface preparation (will be used later)

Here we prepare two producer services user-app, and goods-app each service provides an interface to test the load balancing effect.

user-app (starts two instances)

Ports: 8310, 8311

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

goods-app (starts an instance)

Port: 8320

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

Consumers use RestTemplate to integrate Ribbon test interface calls

The principle of using RestTemplate and OpenFeign to integrate Ribbon is similar. If you use OpenFeign to study Ribbon interference, it will be clearer to use RestTemplate here.

POM

The package version can be based on your own project. The spring-boot-starter-parent version I use here is 2.3.12.RELEASE, the spring-cloud-dependencies version is Hoxton.SR12, and the spring-cloud-alibaba-dependencies version is 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>-->

Configure RestTemplate to use Ribbon load balancer

In the SpringCloud project, you need to define a RestTemplate Bean yourself. Use the @LoadBalanced tag to use the Ribbon for load balancing. Loading will be done in org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration. If you want to understand the loading process, you can Take a look at this article " RestTemplate Integrated Ribbon Source Code Analysis in SpringCloud "

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

Test interface call

    @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. Configuration explanation (some configurations will analyze the source code)

2.1 Basic configuration (timeout, retry configuration)

Note : When RestTemplate is integrated with Ribbon, it is invalid to only configure the global configuration file of Ribbon to implement the timeout retry function. You must add the spring- retry package, and you must also set the link timeout and request timeout for RestTemplate separately. RestTemplate is not Ribbon's timeout global configuration will be used.

  • 1、POM
        <dependency>
            <artifactId>spring-retry</artifactId>
            <groupId>org.springframework.retry</groupId>
        </dependency>
  • 2, place 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 global parameter configuration
ribbon:
  # ConnectTimeout: 1000 #链接超时 RestTemplate是不会使用这个配置的
  # ReadTimeout: 1000 #读取超时  RestTemplate是不会使用这个配置的
  MaxAutoRetries: 0 #同一台实例最大重试次数,不包括首次调用 默认0
  MaxAutoRetriesNextServer: 1 #服务实例切换重试次数 默认1
  OkToRetryOnAllOperations: true #是否开启重试机制 默认false  需要注意一点 只有GET请求才会重试
  retryableStatusCodes: 200,500,503 #需要重试的响应状态 这里设置一个200方便测试,如果不配置请求服务实例报错的时候也会重试
core source code
loading process

1. In the RibbonAutoConfiguration configuration class, it will be judged whether the system has introduced the RetryTemplate class. This class is in the spring-retry package. If it is introduced, it will load a LoadBalancedRetryFactory into the IOC. This will be used in the subsequent LoadBalancerAutoConfiguration object.
insert image description here
2. There will be a similar process in LoadBalancerAutoConfiguration . If the RetryTemplate class is introduced, the retry load balancing interceptor configuration class RetryInterceptorAutoConfiguration will be loaded, and the ordinary LoadBalancerInterceptorConfig will not be loaded. If the RetryTemplate class is not introduced, the LoadBalancerInterceptorConfig will be loaded . This class The RetryLoadBalancerInterceptor loaded in is an interceptor with retry function. If the RetryTemplate class
insert image description here
is not introduced, it will be loadedLoadBalancerInterceptorConfig has no retry functionality.
insert image description here

Configuration items use source code positioning
  • 1. RestTemplate connection timeout and request timeout set by SimpleClientHttpRequestFactory

    • In the doExecute method of RestTemplate, you can see a createRequest operation, which will call the createRequest method of the parent abstract class HttpAccessor . Here, a requestFactory will be obtained, which is the SimpleClientHttpRequestFactory object we set when creating the RestTemplate . The ClientHttpRequest object created by this object The specific implementation class uses InterceptingClientHttpRequest .
      insert image description here
      insert image description here
      insert image description here
  • 2. The retry parameters set by the ribbon global configuration are the maximum number of retries, the number of service instance switching retries, and whether to enable the retry mechanism switch

    • The retry information is loaded in the RibbonClientConfiguration configuration class. A DefaultLoadBalancerRetryHandler is registered through the retryHandler method , and then the RetryHandler is set to the RibbonLoadBalancerContext in the ribbonLoadBalancerContext method . In the subsequent process, the number of retries and services will be obtained through the RibbonLoadBalancerContext object. Enable retry configuration
      insert image description here
      insert image description here
  • 3. Response status that needs to be retried

    • This response status will create a RibbonLoadBalancedRetryPolicy object in the createRetryPolicy method of the RibbonLoadBalancedRetryFactory , and read it in the constructor and load it into the retryableStatusCodes collection. After the request response, the status list will be judged and retried if it is included.
      insert image description here
      insert image description here

2.2 Starvation loading configuration

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

The hungry loading configuration will be loaded in the RibbonAutoConfiguration automatic configuration class through @ConditionalOnProperty("ribbon.eager-load.enabled") , and will be loaded when the project starts according to the service name configured in ribbon.eager-load.clients .
insert image description here

2.3 Refresh service list interval

ribbon:
  ServerListRefreshInterval: 5000 #刷新所服务列表间隔时间 默认30s
core source code
  • The configuration of the time interval for refreshing the service list is also registered in the RibbonClientConfiguration configuration class. Register a PollingServerListUpdater object through the ribbonServerListUpdater method to manage the service list. The start method of the PollingServerListUpdater is the core method for regularly refreshing the service list, which will be in the restOfInit of the DynamicServerListLoadBalancer method call
    insert image description here
    insert image description here

2.4 Load Balancing Rule Configuration

via global configuration (this approach has problems)

PS: There is a big pit here. Through this kind of global configuration, the load balancer will not take effect at all. You need to use the configuration class configuration or configure the rules of each service separately. Using the configuration class configuration also has a big pit that will appear for all services. It is the same ILoadBalancer instance. In the Ribbon, each service should have its own full instance including ILoadBalancer, IRule, IClientConfig, ServerListUpdater, etc. The Rule of each service must be configured separately

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

Reason Analysis:
This configuration will indeed be read into IClientConfig , but each service will call a this.propertiesFactory.isSet(IRule.class, name) when loading its own IRule , and will obtain the individual configuration of each service. If no separate configuration is set, then ZoneAvoidanceRule will be used as the load balancer. A getClassName is called in the isSet method of propertiesFactory . When obtaining the className , it is the obtained separate service setting (name + "." + NAMESPACE + "." + classNameProperty) -> (user-app.ribbon.NFLoadBalancerRuleClassName)
insert image description here
insert image description here

via local configuration
user-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

3. Extension point

3.1 Subscribe to Nacos to refresh the service list in real time

  • If you want to subscribe to Nacos and refresh the service list in real time, you can solve two problems:
    • 1. How to subscribe to Nacos to obtain the service instance information of going online and offline
    • 2. How to refresh the service list in Ribbon

The code is directly implemented here. The code implementation is relatively simple, but the principle involves more content. I will not describe too much here. Subscribe to Nacos to obtain online and offline service instance information. This is very easy to find online. The focus is on Ribbon. If you are interested How to load the Ribbon in Spring Cloud is not very clear, it is difficult to do, here is a brief introduction, if the Ribbon is integrated through the starter package in Spring Cloud, a SpringClientFactory will be loaded, this factory class is the most core class, and each service will create its own The Spring context is independent of the main Spring context, that is to say, it cannot be found in the main IOC at all, and the Spring context of each service is created by the getInstance method of SpringClientFactory. SpringClientFactory integrates NamedContextFactory and will create each The Spring context of a service is stored in the contexts attribute, and the Spring context of the corresponding service can be obtained through getInstance of SpringClientFactory. This context contains all Ribbon components of a service, such as: ILoadBalancer, IRule, IClientConfig, ServerListUpdater, etc., each Each service is independent. For example, user-app has user-app Spring context goods-app also has its own independent context, so it has its own independent Ribbon components, but these components cannot be registered in the Spring main context, otherwise in Loading the Spring context of each service will obtain the bean of the parent container. There are many things involved here. If you are interested, you can read the source code by yourself. The premise of reading is that you have the source code foundation of Spring and SpringBoot, otherwise it will look difficult.

@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 Custom load balancer

  • 1. Custom random load balancer code implementation
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. Configure the user-app service to use a custom load balancer (it can only be configured independently, and global configuration cannot be used otherwise there will be problems)
user-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.kerwin.order.ribbon.config.MyRandomRule

3.3 Realize grayscale publishing through Ribbon

There are two ways to use Ribbon to implement grayscale publishing. The first is through ServerListFilter, and the second is through load balancer IRule. The core idea is to use Nacos Metadata to store a version value in Metadata. To determine what version of the service to call, here is a simple implementation through ServerListFilter that can be expanded according to your own needs. The principle of implementing through IRule is similar, but the timing is different. ServerListFilter will filter the service list when pulling configuration information, and IRule is in It will only be judged when an RPC request is initiated, and you can choose a solution according to your actual business.

  • 1. Code implementation
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. Configure the call user-app service to use a custom interceptor

    • Producer user-app service configuration Nacos metadata
    spring:
      application:
        name: user-app
      cloud:
        nacos:
          discovery:
            namespace: springcloud-ribbon-example
            metadata:
              version: V2
          server-addr: 119.91.223.226:8848
    
    • Consumers configure custom interceptors (can only be configured independently, global configuration cannot be used or there will be problems)
    	user-app:
    	  ribbon:
    	    NIWSServerListFilterClassName: com.kerwin.order.ribbon.config.MyServerListFilter
    

Guess you like

Origin blog.csdn.net/weixin_44606481/article/details/131619483