Spring Cloud series:

  • Implement Spring Cloud Gateway dynamic routing and built-in filters
  • The meeting point of Spring Cloud Gateway and Spring WebFlux
  • Spring Cloud Gateway filter workflow

The introduction

In our last Spring Cloud Gateway filter workflow We said some key filters, including ReactiveLoadBalancerClientFilter is responsible for the client is responsible for the equilibrium of a filter processing.

Today we’ll take a closer look at how this filter interacts with the Loadbalancer component to achieve client load balancing

ReactiveLoadBalancerClientFilterSource code analysis

ReactiveLoadBalancerClientFilter is instantiated in the class of the GatewayReactiveLoadBalancerClientAutoConfiguration configuration, At the time of instantiation by constructor injection has an important example of this: LoadBalancerClientFactory.

@Bean
@ConditionalOnBean(LoadBalancerClientFactory.class)
@ConditionalOnMissingBean(ReactiveLoadBalancerClientFilter.class)
@ConditionalOnEnabledGlobalFilter
public ReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, GatewayLoadBalancerProperties properties) {
   return new ReactiveLoadBalancerClientFilter(clientFactory, properties);
}
Copy the code

Simple said there why LoadBalancerClientFactory here, as the name implies, it is a client load balancer factory class. Gateway proxy agent will according to the routing configuration information to the downstream service, every routing information corresponding to one or more downstream services, and LoadBalancerClientFactory is according to the routing information, to produce the corresponding client side load balancing instance, namely: Each routing information has a corresponding client load balancing instance. There are multiple client load balancing instances

We look back the ReactiveLoadBalancerClientFilter# filter method, according to the request message to the request uri, according to the analytical serviceId.

URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String serviceId = requestUri.getHost();
Copy the code

Then in ReactiveLoadBalancerClientFilter# choose service instances within the method of choice.

private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
                Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
        // Return the corresponding LoadBalancer instance according to serviceI
        ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance(serviceId,
                        ReactorServiceInstanceLoadBalancer.class);
        if (loadBalancer == null) {
                throw new NotFoundException("No loadbalancer available for " + serviceId);
        }
        supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
        // Select the service instance
        return loadBalancer.choose(lbRequest);
}
Copy the code

After the Loadbalancer instance is selected to the corresponding downstream service instance, the uri is replaced to get the actual request address, which is finally passed to the next filter.

// Service instance returned by load balancing
ServiceInstance retrievedInstance = response.getServer();
URI uri = exchange.getRequest().getURI();
/ / rewrite scheme
String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
if(schemePrefix ! =null) {
   overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
      overrideScheme);
// Parse the actual URL to be proxied
URI requestUrl = reconstructURI(serviceInstance, uri);
Copy the code

Loadbalancer – Assembly process

In the last section in ReactiveLoadBalancerClientFilter# we saw on choose method of client side load balancing is obtained through LoadBalancerClientFactory class instance, through the interrupt point to know the specific instance is the default: RoundRobinLoadBalancer, it is obtained by AnnotationConfigApplicationContext. The code is as follows:

public <T> T getInstance(String name, Class<T> type) {
        AnnotationConfigApplicationContext context = getContext(name);
        try {
                return context.getBean(type);
        }catch (NoSuchBeanDefinitionException e) {
        }
        return null;
}
Copy the code

Next, we went through the Loadbalancer component source code and found two configuration classes:

  • LoadBalancerClientConfigurationClient load balancing configuration
  • LoadBalancerAutoConfigurationLoad balancing is automatically configured

LoadBalancerClientFactory class is instantiated in LoadBalancerAutoConfiguration configuration class, it also holds a LoadBalancerClientSpecification list, It implements NamedContextFactory interface Specification, here is in order to achieve the different routing will have different instances of client side load balancing.

Main configuration LoadBalancerClientConfiguration configuration class:

  • ReactorLoadBalancerClient load balancing instance
  • ServiceInstanceListSupplierService instance list provider, which is used to obtain the service instance list based on the serviceId.

The code is as follows:

@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);
}
Copy the code
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(DefaultConfigurationCondition.class)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) {
   return ServiceInstanceListSupplier.builder().withDiscoveryClient().withCaching().build(context);
}
Copy the code

Loadbalancer – ReactorLoadBalancer instantiation

In the previous section, we found where the ReactorLoadBalancer instance is assembled, but is it that simple? Clearly not!

ReactorLoadBalancer instances are multi-instance, and different routes generally have different client load balancing instances. So how does it work?

In LoadBalancerAutoConfiguration class, we saw a note: LoadBalancerClients, it imports the a LoadBalancerClientConfigurationRegistrar class, it is registered in this class LoadBalancerClientSpecification instance. The code is as follows:

	private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(LoadBalancerClientSpecification.class);
		builder.addConstructorArgValue(name);
		builder.addConstructorArgValue(configuration);
		registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification", builder.getBeanDefinition());
	}
Copy the code

This stuff is LoadBalancerClientFactory configuration information, the default is null.

This time look back LoadBalancerAutoConfiguration class instantiation LoadBalancerClientFactory when a ObjectProvider < List < LoadBalancerClientSpecificatio The n>> instance is derived from the LoadBalancerClients annotation.

At this point, LoadBalancerClientFactory instantiation is good, and we also know that ReactorLoadBalancer by LoadBalancerClientFactory# getInstances access, here is its parent class: The NamedContextFactory method gets the underlying code for the ReactorLoadBalancer instance as follows:

protected AnnotationConfigApplicationContext createContext(String name) {
    // Create a Spring child Context
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   // Determine whether to instantiate within the configuration class, and register if so
   if (this.configurations.containsKey(name)) {
      for(Class<? > configuration :this.configurations.get(name).getConfiguration()) { context.register(configuration); }}// Determine whether to instantiate within the default configuration and register if so
   for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for(Class<? > configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } context.register(PropertyPlaceholderAutoConfiguration.class,this.defaultConfigType);
   // Add attributes to the environment variable, key: "loadbalancer.client.name", value: serviceId
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,
         Collections.<String, Object>singletonMap(this.propertyName, name)));
   // Set its parent context
   if (this.parent ! =null) {
      context.setParent(this.parent);
      context.setClassLoader(this.parent.getClassLoader());
   }
   context.setDisplayName(generateDisplayName(name));
   context.refresh();
   return context;
}
Copy the code

This is according to create a AnnotationConfigApplicationContext serviceId, so as to realize different route has different Spring container, and then from the container to get the different instances of client side load balancing.

In this container has been created, and from the container get ReactorLoadBalancer instance, when the code is reached the configuration class: we just see LoadBalancerClientConfiguration.

The configuration class here uses annotations: @configuration (proxyBeanMethods = false), that is, when the first instance is obtained, it will not be obtained from the parent container, but directly new an instance, and then put it into the child container, the next time from the child container cache, Finally, different routes have different client load balancing instances!

Consider: if at first the LoadBalancerClientConfiguration configuration to LoadBalancerClientSpecification instance configuration list, then each time from the container to obtain an instance of the same instance?

Loadbalancer – RoundRobinLoadBalancer source code

In the previous section, we finally learned how to instantiate client load balancing and how to implement different client load balancing instances for different routes. We also learned that the default client load balancing instance is RoundRobinLoadBalancer.

RoundRobinLoadBalancer#getInstanceResponse maintains an incrementally increased member variable and then evaluates to the list of instances.

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
                if (log.isWarnEnabled()) {
                        log.warn("No servers available for service: " + serviceId);
                }
                return new EmptyResponse();
        }
        / / training in rotation
        int pos = Math.abs(this.position.incrementAndGet());
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
}
Copy the code

Loadbalancer – Service discovery

To implement client load balancing, you first need to get a list of service instances from the registry, where the service discovery component is essential. So how does it combine with the Loadbalancer component?

At the time of instantiation ReactorLoadBalancer, we will find that its a ObjectProvider obtained through LoadBalancerClientFactory < ServiceInstanceListSupplier > instance. The instance is in ReactiveSupportConfiguration# discoveryClientServiceInstanceListSupplier method within the assembly. The code is as follows:

    @Bean
    @ConditionalOnBean(ReactiveDiscoveryClient.class)
    @ConditionalOnMissingBean
    @Conditional(DefaultConfigurationCondition.class)
    public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) {
            return ServiceInstanceListSupplier.builder().withDiscoveryClient().withCaching().build(context);
    }
Copy the code

You can see that through the builder pattern, service discovery components and cache components are added to the service provider using the proxy pattern. The service discovery component handles the following:

public ServiceInstanceListSupplierBuilder withDiscoveryClient(a) {
        if(baseCreator ! =null && LOG.isWarnEnabled()) {
                LOG.warn("Overriding a previously set baseCreator with a ReactiveDiscoveryClient baseCreator.");
        }
        // In functional form, the concrete instance is returned only when applied
        this.baseCreator = context -> {
                ReactiveDiscoveryClient discoveryClient = context.getBean(ReactiveDiscoveryClient.class);

                return new DiscoveryClientServiceInstanceListSupplier(discoveryClient, context.getEnvironment());
        };
        return this;
}
Copy the code

The last

Now that we’ve read the basic workflow of the Loadbalancer component, you have a better understanding of Spring Cloud Loadbalancer.

The instantiation of ReactorLoadBalancer is difficult to understand, so we need to look at the NamedContextFactory function to better understand the Spring sub-container and other relevant knowledge.

Above, if anything is wrong. Add your personal wechat friends to discuss: DaydayCoupons or follow your wechat public account AnyinWechart to exchange ideas.