Ribbon principle analysis

Posted by kitcorsa on Mon, 03 Jan 2022 11:17:54 +0100

1, Introduction

Ribbon is a component for load balancing on the client side. It is used for load balancing between services in spring cloud microservices. The default is polling algorithm. You can configure other algorithms and customize load balancing algorithms.

Client load balancing: a request has declared which service to call at the client, and then calls one of the multiple node services through a specific load balancing algorithm.

Load balancing on the server side: a request first passes through the proxy server, such as nginx, and then the proxy server reversely proxies the server side through the load balancing algorithm to complete the service call.

2, Automatic assembly

If you want to use ribbon load balancing, you can usually annotate @ LoadBalanced directly, and then use restTemplate to call, which will automatically trigger the load balancing algorithm. Of course, if we use components such as feign and have automatically integrated ribbon, we don't need to do so.

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

@Dbalanceload annotation

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {

}

An empty annotation has no practical significance.

Spring SPI mechanism, configuration file spring Factories, you can find the automatic assembly class loadbalancenautoconfiguration

@Configuration(proxyBeanMethods = false)
// The current environment requires RestTemplate.class
@ConditionalOnClass(RestTemplate.class)
// The current environment is required to have LoadBalancerClient Implementation class of interface
@ConditionalOnBean(LoadBalancerClient.class)
// Initialize profile LoadBalancerRetryProperties
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    @LoadBalanced
    @Autowired(required = false)
    // 1,@AutoWired Collection classes are also automatically loaded list,Will be appropriate RestTemplate Add to restTemplates in
    // And as for what to load RestTemplate,That's the label@LoadBalanced of RestTemplate
    // Above we see@LoadBalanced There is one@Qualifier Is the meaning of special labels, so ordinary ones are not added@LoadBalanced
    // Will not be added to restTemplates Medium
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    @Bean
    // 2,SmartInitializingSingleton The implementation class of the interface will be called after the project is initialized afterSingletonsInstantiated method
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
            final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
    }

    @Bean
    //Same type bean Only one can be registered
    @ConditionalOnMissingBean
    // 3,LoadBalancerRequestFactory Created
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {

        // 4,take LoadBalancerClient Interface is created in the implementation class and method 3 LoadBalancerRequestFactory
        // Inject into the method and become LoadBalancerInterceptor Interceptor parameters
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        @Bean
        @ConditionalOnMissingBean
         // 5,Created in method 4 LoadBalancerInterceptor Will be injected as method parameters
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            // customize Method will be replaced by method 2 afterSingletonsInstantiated()Traversal call
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }

    }

    /**
     * Configure retry mechanism
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RetryTemplate.class)
    public static class RetryAutoConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryFactory loadBalancedRetryFactory() {
            return new LoadBalancedRetryFactory() {
            };
        }

    }

    /**
     * Auto configuration for retry intercepting mechanism.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RetryTemplate.class)
    public static class RetryInterceptorAutoConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRetryProperties properties,
                LoadBalancerRequestFactory requestFactory,
                LoadBalancedRetryFactory loadBalancedRetryFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
                    requestFactory, loadBalancedRetryFactory);
        }

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }

    }

}

3, Request processing

After the project is started, an interceptor will be added when creating the resttemplate. When the resttemplate is used to send a request, the load balancing policy will be triggered through the interceptor. Take getForObject method call as an example.

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
    RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
    HttpMessageConverterExtractor<T> responseExtractor =
        new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
    return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}
 
// execute
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
                     ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
 
    URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    return doExecute(expanded, method, requestCallback, responseExtractor);
}
 
//doExecute
protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback,
                          ResponseExtractor<T> responseExtractor) throws RestClientException {
 
    ...
    ClientHttpResponse response = null;
    try {
        // 1.Create a request request
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        // 2.Execute the request
        response = request.execute();
        // 3.Encapsulate the response results
        handleResponse(url, method, response);
        if (responseExtractor != null) {
            return responseExtractor.extractData(response);
        }
        else {
            return null;
        }
    }
    ...
}

1. createRequest(url, method) creates a request

protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
    // The point is here
    ClientHttpRequest request = getRequestFactory().createRequest(url, method);
    if (logger.isDebugEnabled()) {
        logger.debug("Created " + method.name() + " request for \"" + url + "\"");
    }
    return request;
}
 
//getRequestFactory
public ClientHttpRequestFactory getRequestFactory() {
    ClientHttpRequestFactory delegate = super.getRequestFactory();
    // RestTemplate Interceptor is not empty
    if (!CollectionUtils.isEmpty(getInterceptors())) {
        return new InterceptingClientHttpRequestFactory(delegate, getInterceptors());
    }
    else {
        return delegate;
    }
}
 
// InterceptingClientHttpRequestFactory.createRequest(url, method)
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
    // That's what you end up returning request
    return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
}

2. Execute the request execute()

//Current request by InterceptingClientHttpRequest
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    // 1.iterator Interceptor set
    // private final Iterator<ClientHttpRequestInterceptor> iterator;
    if (this.iterator.hasNext()) {
        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
        // 2.Interceptor by interceptor, let's look at the most important LoadBalancerInterceptor
        return nextInterceptor.intercept(request, body, this);
    }
    else {
        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
        for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
            List<String> values = entry.getValue();
            for (String value : values) {
                delegate.getHeaders().add(entry.getKey(), value);
            }
        }
        if (body.length > 0) {
            StreamUtils.copy(body, delegate.getBody());
        }
        return delegate.execute();
    }
}
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                                    final ClientHttpRequestExecution execution) throws IOException {
    final URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    // Real execution method
    // private LoadBalancerClient loadBalancer; 
    // LoadBalancerClient The default implementation class is RibbonLoadBalancerClient
    return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
 
// RibbonLoadBalancerClient.execute()
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    // 1.According to the user's request serviceId To get specific information LoadBalanced
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
 
    // 2.Get specific server(That is, the specific service information of which port number of which server to locate)
    Server server = getServer(loadBalancer);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,serviceId),
                        serverIntrospector(serviceId).getMetadata(server));
    // 3.implement HTTP request
    return execute(serviceId, ribbonServer, request);
}

Through the analysis of this method, we can see that a series of algorithms are used to obtain the specific host and port of the service according to the serviceId (i.e. service name) entered by the user, then re encapsulate the HTTP request, and finally execute the HTTP request.

2.1. getLoadBalancer() gets the load balancer

protected ILoadBalancer getLoadBalancer(String serviceId) {
    return this.clientFactory.getLoadBalancer(serviceId);
}
 
// SpringClientFactory.getLoadBalancer()
public ILoadBalancer getLoadBalancer(String name) {
    return getInstance(name, ILoadBalancer.class);
}
 
// SpringClientFactory.getInstance()
public <C> C getInstance(String name, Class<C> type) {
    C instance = super.getInstance(name, type);
    if (instance != null) {
        return instance;
    }
    IClientConfig config = getInstance(name, IClientConfig.class);
    return instantiateWithConfig(getContext(name), type, config);
}
 
// NamedContextFactory.getInstance()
public <T> T getInstance(String name, Class<T> type) {
    AnnotationConfigApplicationContext context = getContext(name);
    if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                                                            type).length > 0) {
        // This is the main sentence. Get from the container ILoadBalancer The default implementation is
        // ZoneAwareLoadBalancer
        return context.getBean(type);
    }
    return null;
}

2.2,getServer(loadBalancer)

protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        // The specific implementation is ZoneAwareLoadBalancer.chooseServer()
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }
 
    // ZoneAwareLoadBalancer.chooseServer()
    public Server chooseServer(Object key) {
        // 1.Because the author tested server,Available zone It's one, so I'll go straight super.chooseServer()
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
        //If more region,The following method will be followed and commented out temporarily
        ...
    }
        
    //BaseLoadBalancer.chooseServer()
   public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                // rule by ZoneAvoidanceRule
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }
        
    //PredicateBasedRule.choose(key)
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        // The point is here, from all server According to the corresponding rule To get a specific server
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }

The main function of getServer() is to select a specific Server according to specific rule s. The important implementation is actually in this method

2.3,RibbonLoadBalancerClient.execute(serviceId, ribbonServer, request) executes the HTTP request

public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    ...
    RibbonLoadBalancerContext context = this.clientFactory
        .getLoadBalancerContext(serviceId);
    RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
 
    try {
        // The core method here is a callback method,
        // Specifically, callback LoadBalancerRequestFactory.createRequest()Medium apply()method
        T returnVal = request.apply(serviceInstance);
        statsRecorder.recordStats(returnVal);
        return returnVal;
    }
    ...
}
 
    // Callback method
    public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
                                                                 final byte[] body, final ClientHttpRequestExecution execution) {
        return new LoadBalancerRequest<ClientHttpResponse>() {
 
            @Override
            // The callback method is here
            public ClientHttpResponse apply(final ServiceInstance instance)
            throws Exception {
                HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
                if (transformers != null) {
                    for (LoadBalancerRequestTransformer transformer : transformers) {
                        serviceRequest = transformer.transformRequest(serviceRequest, instance);
                    }
                }
                // Method to be implemented
                return execution.execute(serviceRequest, body);
            }
 
        };
    }
 
    //InterceptingRequestExecution.execute()
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
        if (this.iterator.hasNext()) {
            ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
            return nextInterceptor.intercept(request, body, this);
        }
        // Note: there is no iterator,Direct execution request request
        else {
            // 1.according to URI Get the request and encapsulate the header
            ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
            for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
                List<String> values = entry.getValue();
                for (String value : values) {
                    delegate.getHeaders().add(entry.getKey(), value);
                }
            }
            if (body.length > 0) {
                StreamUtils.copy(body, delegate.getBody());
            }
            // 2.The essence is right HttpURLConnection Implementation of
            return delegate.execute();
        }
    }
}

3. Encapsulation response

    protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
        //Gets the processor that handled the error
        ResponseErrorHandler errorHandler = getErrorHandler();
        boolean hasError = errorHandler.hasError(response);
        if (logger.isDebugEnabled()) {
            try {
                int code = response.getRawStatusCode();
                //Parse response status
                HttpStatus status = HttpStatus.resolve(code);
                logger.debug("Response " + (status != null ? status : code));
            }
            catch (IOException ex) {
            }
        }
        if (hasError) {
            //Handling the corresponding errors
            errorHandler.handleError(url, method, response);
        }
    }

Summary:

1. Create RestTemplate
2. After adding the ribbon dependency, the LoadBalancerInterceptor interceptor will be automatically added to the RestTemplate when the project starts
3. When the user initiates a request according to the RestTemplate, the request will be forwarded to the loadbalancerinceptor for execution. The interceptor will obtain the application server IP and port corresponding to the request according to the specified load balancing method
4. Re encapsulate the request according to the obtained IP and port, send the HTTP request and return the specific response

4, Custom load balancer

Implement the interface IRule, and then take a look at some default rules implemented by ribbon, such as RandomRule. Refer to his writing method to implement the load balancing strategy you want, and finally inject it into the Spring container.

reference resources: https://blog.csdn.net/qq_26323323/article/details/81327669

Topics: Spring Cloud