preface
In this chapter, I will explain the specific load balancing rules. First, I will express my personal understanding of the spring cloud loadbalancer load balancing rules:
- Get the corresponding instance collection according to the service ID, which contains some rules: get from the registry? Same region priority? Cache? etc.
- Select the finally determined instance from the above instance set, which contains the rule: poll for? Random acquisition? etc.
The corresponding interfaces are ServiceInstanceListSupplier and ReactorLoadBalancer
ServiceInstanceListSupplier
public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> { // Service id String getServiceId(); // Instance list default Flux<List<ServiceInstance>> get(Request request) { return get(); } // constructor static ServiceInstanceListSupplierBuilder builder() { return new ServiceInstanceListSupplierBuilder(); } }
This interface provides a list of qualified instances and provides the builder method to return the ServiceInstanceListSupplierBuilder instance, which is used to construct the ServiceInstanceListSupplier
The whole implementation class of ServiceInstanceListSupplier is rx programming style, but the core logic is not difficult to understand. Here are some implementation classes for a brief understanding
DiscoveryClientServiceInstanceListSupplier
public class DiscoveryClientServiceInstanceListSupplier implements ServiceInstanceListSupplier { // ... public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate, Environment environment) { this.serviceId = environment.getProperty(PROPERTY_NAME); resolveTimeout(environment); this.serviceInstances = Flux.defer(() -> Flux.just(delegate.getInstances(serviceId))) .subscribeOn(Schedulers.boundedElastic()).timeout(timeout, Flux.defer(() -> { logTimeout(); return Flux.just(new ArrayList<>()); })).onErrorResume(error -> { logException(error); return Flux.just(new ArrayList<>()); }); }
The main logic in the construction method is equivalent to this serviceInstances = discoveryClient. Getinstances (serviceid), which is easy to understand: pull the instance list from the registry
DelegatingServiceInstanceListSupplier
public abstract class DelegatingServiceInstanceListSupplier implements ServiceInstanceListSupplier, InitializingBean, DisposableBean { protected final ServiceInstanceListSupplier delegate; public DelegatingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) { Assert.notNull(delegate, "delegate may not be null"); this.delegate = delegate; } // ... }
The decoration layer embeds a proxy object, which is generally the DiscoveryClientServiceInstanceListSupplier to obtain the instance list. The decoration logic is to filter the corresponding list
ZonePreferenceServiceInstanceListSupplier
public class ZonePreferenceServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { // ... // delegate. filteredByZone filtering after get @Override public Flux<List<ServiceInstance>> get() { return getDelegate().get().map(this::filteredByZone); } private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) { if (zone == null) { zone = zoneConfig.getZone(); } if (zone != null) { List<ServiceInstance> filteredInstances = new ArrayList<>(); /** * Obtain the zone in the instance for matching filtering */ for (ServiceInstance serviceInstance : serviceInstances) { String instanceZone = getZone(serviceInstance); if (zone.equalsIgnoreCase(instanceZone)) { filteredInstances.add(serviceInstance); } } if (filteredInstances.size() > 0) { return filteredInstances; } } return serviceInstances; } // ... }
Region first rule: delegate gets the instance list and matches the zone attribute according to the instance metadata
other
There are other implementation classes such as:
- HintBasedServiceInstanceListSupplier is filtered according to the hint attribute of the header. The default attribute name is X-SC-LB-Hint
- Sameinstance preferenceserviceinstancelistsupplier returns the last returned instance
- wait
ReactorLoadBalancer
public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> { @SuppressWarnings("rawtypes") Mono<Response<T>> choose(Request request); default Mono<Response<T>> choose() { return choose(REQUEST); } }
This interface selects the final target from the instances returned by ServiceInstanceListSupplier. Spring cloud loadbalancer only provides two implementations by default:
- RandomLoadBalancer: random selection
- Roundrobin loadbalancer: polling
Code details
LoadBalancerClientConfiguration
@Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled public class LoadBalancerClientConfiguration { /** * Instance selection rule: default polling */ @Bean @ConditionalOnMissingBean public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RoundRobinLoadBalancer( loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } /** * WebFlux Default ServiceInstanceListSupplier configuration in environment */ @Configuration(proxyBeanMethods = false) @ConditionalOnReactiveDiscoveryEnabled @Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER) public static class ReactiveSupportConfiguration { // ... } /** * Configuration in common web Environment */ @Configuration(proxyBeanMethods = false) @ConditionalOnBlockingDiscoveryEnabled @Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER + 1) public static class BlockingSupportConfiguration { /** * Default configuration: withblockingdiscoveryclient() withCaching() * Service discovery, caching */ @Bean @ConditionalOnBean(DiscoveryClient.class) @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default", matchIfMissing = true) public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context); } /** * Regional priority configuration */ @Bean @ConditionalOnBean(DiscoveryClient.class) @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "zone-preference") public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withZonePreference() .withCaching().build(context); } /** * Health check configuration * @param context * @return */ @Bean @ConditionalOnBean({ DiscoveryClient.class, RestTemplate.class }) @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "health-check") public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withBlockingHealthChecks() .build(context); } // ... } // ... }
Now look back at the configuration class LoadBalancerClientConfiguration registered by default under each LoadBalancerClient container instance, as shown in the code:
- The default instance selection rule is polling
- The rules for obtaining the corresponding instance list depend on spring cloud. loadbalancer. Configurations property
BlockingLoadBalancerClient#execute
@Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { // ... // Instance selection ServiceInstance serviceInstance = choose(serviceId, lbRequest); // ... return execute(serviceId, serviceInstance, lbRequest); } ----------------------------------------------- @Override public <T> ServiceInstance choose(String serviceId, Request<T> request) { /** * Gets the ReactiveLoadBalancer instance in the serviceId container */ ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId); if (loadBalancer == null) { return null; } Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block(); if (loadBalancerResponse == null) { return null; } return loadBalancerResponse.getServer(); }
Finally, return to the BlockingLoadBalancerClient#execute logic (LoadBalancerClient installed by default in the container):
- Logic is nothing more than selecting the final instance to execute the request
- The underlying logic is to obtain the corresponding ServiceInstanceListSupplier and ReactiveLoadBalancer from the isolated container to select the instance
Best practices
At the end of the article, I'd like to talk about what I think are the best practices for configuration:
- In the client dimension, each client isolates a configuration and finally integrates it into a configuration class through the @ LoadBalancerClients annotation. The specific configuration is recommended to be maintained in the form of internal classes. The disadvantage is that when the rules of the client change, the corresponding configuration class needs to be modified
- In the configuration dimension, each configuration is isolated. In this way, when integrated into a configuration class through the @ LoadBalancerClients annotation, the client can select the corresponding configuration. The disadvantage is that there are many configuration combinations, so there may be many configuration classes, but the advantage is that the change of configuration only needs to modify the attribute value of @ LoadBalancerClients
Examples
client dimension
@LoadBalancerClients({ @LoadBalancerClient(value = "eureka-client-1", configuration = ClietConfig.EurekaClient1Config.class) , @LoadBalancerClient(value = "eureka-client-2", configuration = ClietConfig.EurekaClient2Config.class) }) public class ClietConfig { static class EurekaClient1Config { // ... } static class EurekaClient2Config { // ... } }
Configuration dimension
@LoadBalancerClients({ @LoadBalancerClient(value = "eureka-client-1", configuration = LoadBalanceConfig.RandomLoadBalancerConfig.class) , @LoadBalancerClient(value = "eureka-client-2", configuration = LoadBalanceConfig.ZonePerferServiceListConfig.class) }) public class LoadBalanceConfig { static class RandomLoadBalancerConfig { // ... } static class HintServiceListConfig { // ... } // ... }
It is worth noting that the load balancing configuration class does not need to be added @Configuration Annotation, otherwise it will Registered into the current container at the same time
summary
So far, the basic implementation principle of spring cloud loadbalancer has been interpreted. I think the logic of load balancing is important. What interests me more is that the isolated implementation of all loadbalancerclients by spring cloud is really ingenious and provides very strong scalability