Configuring spring-cloud loadbalancer without autoconfiguration

Viewed 1440

I've read whole documentation, tutorial [1], and spend several hours in sources, but I still do not understand how to configure loadbalancer, especially if I don't use magic annotations.

I have following configuration:

@Configuration
public class AppConfig {

    public static final String SERVICE_ID = "service";

    @Primary
    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier() {
        return ServiceInstanceListSuppliers.from(SERVICE_ID,
                new DefaultServiceInstance(SERVICE_ID + "1", SERVICE_ID, "localhost", 8886, false),
                new DefaultServiceInstance(SERVICE_ID + "2", SERVICE_ID, "localhost", 8887, false));
    }

    @Bean
    public LoadBalancerClientFactory loadBalancerClientFactory() {
        return new LoadBalancerClientFactory();
    }

    @Bean
    public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(LoadBalancerProperties properties) {
        return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerClientFactory(), properties);
    }
}

and use bean loadBalancerExchangeFilterFunction as:

WebClient.builder()
            .baseUrl("http://service/test-consumer")
            .filter(lbFunction)
            .build();

and it works. The problem is, that it works regardless of what hostname I use. So if I replace hostname "service" with whatever work I like, I will be still sending data to localhost:8886 or localhost:8887.

Can someone explain what is the role of serviceId and how this is paired to collection of DefaultServiceInstance?

(I want to understand the internals, what are the key components, their purpose and their interplay. I'm not primarily looking for magic annotation, but that one actually explained would be also great. Debugging it is really hard, I have several A4 with class diagrams and it still makes no sense at all).

Question: Is there a misconfiguration? What is the purpose of serviceId? It seems that none, as webclient using ReactorLoadBalancerExchangeFilterFunction will create roundrobin loadbalancer over configured ServiceInstances regardless of what is actual hostname used in given webclient.

Question2: how could I create 2 loadbalanced services and control to which service (not node) will request go? Do I need 2 separate webclients or some url pattern(like using serviceId in place of hostname) will do? If I need 2 webclients, how is pairing to DefaultServiceInstance done?

[1] https://spring.io/guides/gs/spring-cloud-loadbalancer/


EDIT:

after suggested update, the configuration looks like:

@Configuration
public class AppConfig {

    @Bean
    public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
        return ServiceInstanceListSupplier.builder()
                .withDiscoveryClient()
                .withHealthChecks()
                .build(context);
    }

    @Bean
    public LoadBalancerClientFactory loadBalancerClientFactory() {
        return new LoadBalancerClientFactory();
    }

    @Bean
    public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(LoadBalancerProperties properties) {
        return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerClientFactory(), properties);
    }
}

application.properties contains:

spring.cloud.discovery.client.simple.instances.complicated[0].uri=http://localhost:8886
spring.cloud.discovery.client.simple.instances.complicated[1].uri=http://localhost:8887

webclient call to URL: http://localhost:8888/test-consumer (ie. hostname not matching serviceID) produces:

o.s.c.l.core.RoundRobinLoadBalancer      : No servers available for service: localhost
eactorLoadBalancerExchangeFilterFunction : LoadBalancer does not contain an instance for the service localhost

webclient call to URL: http://complicated/test-consumer (ie. hostname matching serviceID) produces:

o.s.c.l.core.RoundRobinLoadBalancer      : No servers available for service: complicated
eactorLoadBalancerExchangeFilterFunction : LoadBalancer does not contain an instance for the service complicated

The reason for this is that this.serviceId = environment.getProperty(PROPERTY_NAME); in DiscoveryClientServiceInstanceListSupplier(ReactiveDiscoveryClient,Environment) evaluates as null, thus even though I'm looking for some serviceId, delegate.getInstance is called with null, so no ServiceInstances are found. IF I removed @Bean instanceSupplier, and hope for autoconfiguration do it somehow magically, the this.serviceId = environment.getProperty(PROPERTY_NAME); is somehow magically set, serviceId is propagated correctly, and it works. For calls which leads elsewhere than configured serviceId, it fails saying, that this serviceId is not know, instead of making call.

SO it does mean, that if I configure loadbalancer, I cannot call anything else but (auto)configured services???

2 Answers

The LoadBalancer config should not be in a @Configuration-annotated class; instead, it should be a class passed for config via @LoadBalancerClient or @LoadBalancerClients annotation, as described here.

Also, the only bean you need to instantiate is the ServiceInstanceListSupplier (if you add spring-cloud-starter-loadbalancer, LoadBalancerClientFactory, and ReactorLoadBalancerExchangeFilterFunction will be instantiated by the starter).

So your LoadBalancer configuration class will look like so (without @Configuration):

public class AppConfig {

    @Bean
    public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
        return ServiceInstanceListSupplier.builder()
                .withDiscoveryClient()
                .withHealthChecks()
                .build(context);
    }

}

and your actual @Configuration class (for example, where you configure other webflux-related beans), will have the following annotation: @LoadBalancerClients(defaultConfiguration = AppConfig.class).

Then, if you enable health-checks in the complicated instances, it should work without any problems.

Finally, able to resolve this issue with the below Configuration. not sure why it works only with Non blocking approach and when we pass the new RestTemplate

@Bean
    public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
        return ServiceInstanceListSupplier.builder()
                .withDiscoveryClient()
                .withBlockingHealthChecks(new RestTemplate())//this change
                .build(context);
    } 
Related