Skip to content

Commit 75338cd

Browse files
Zoned ServiceInstanceListSupplier (#658)
* Get ServiceInstance zone from metadata. * Add ZonePreferenceServiceInstanceListSupplier. * Add javadocs and license entries. * Add tests. * Add documentation. * Documentation fix. * Fix after code review.
1 parent be0fa8f commit 75338cd

File tree

3 files changed

+278
-12
lines changed

3 files changed

+278
-12
lines changed

docs/src/main/asciidoc/spring-cloud-commons.adoc

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -831,19 +831,20 @@ that retrieves available instances from Service Discovery using a <<discovery-cl
831831

832832
=== Spring Cloud LoadBalancer integrations
833833

834-
In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used
835-
with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate`. You can see more information and examples of usage
836-
in the following sections:
834+
In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate`.
835+
You can see more information and examples of usage in the following sections:
837836

838837
* <<rest-template-loadbalancer-client,Spring RestTemplate as a Load Balancer Client>>
839838
* <<webclinet-loadbalancer-client, Spring WebClient as a Load Balancer Client>>
840839
* <<webflux-with-reactive-loadbalancer,Spring WebFlux WebClient with `ReactorLoadBalancerExchangeFilterFunction`>>
841840

841+
[[loadbalancer-caching]]
842842
=== Spring Cloud LoadBalancer Caching
843843

844844
Apart from the basic `ServiceInstanceListSupplier` implementation that retrieves instances via `DiscoveryClient` each time it has to choose an instance, we provide two caching implementations.
845845

846846
==== https://github.com/ben-manes/caffeine[Caffeine]-backed LoadBalancer Cache Implementation
847+
847848
If you have `com.github.ben-manes.caffeine:caffeine` in the classpath, Caffeine-based implementation will be used.
848849
See the <<loadbalancer-cache-configuration, LoadBalancerCacheConfiguration>> section for information on how to configure it.
849850

@@ -871,25 +872,66 @@ The default setup includes `ttl` set to 30 seconds and the default `initialCapac
871872
You can also altogether disable loadBalancer caching by setting the value of `spring.cloud.loadbalancer.cache.enabled`
872873
to `false`.
873874

874-
WARNING: Although the basic, non-cached, implementation is useful for prototyping and testing, it's much less efficient
875-
than the cached versions, so we recommend always using the cached version in production.
875+
WARNING: Although the basic, non-cached, implementation is useful for prototyping and testing, it's much less efficient than the cached versions, so we recommend always using the cached version in production.
876+
877+
=== Zone-Based Load-Balancing
878+
879+
To enable zone-based load-balancing, we provide the `ZonePreferenceServiceInstanceListSupplier`.
880+
We use `DiscoveryClient`-specific `zone` configuration (for example, `eureka.instance.metadata-map.zone`) to pick the zone that the client tries to filter available service instances for.
881+
882+
NOTE: You can also override `DiscoveryClient`-specific zone setup by setting the value of `spring.cloud.loadbalancer.zone` property.
883+
884+
NOTE: To determine the zone of a retrieved `ServiceInstance`, we check the value under the `"zone"` key in its metadata map.
885+
886+
The `ZonePreferenceServiceInstanceListSupplier` filters retrieved instances and only returns the ones within the same zone.
887+
If the zone is `null` or there are no instances within the same zone, it returns all the retrieved instances.
888+
889+
In order to use the zone-based load-balancing approach, you will have to instantiate a `ZonePreferenceServiceInstanceListSupplier` bean in a <<custom-loadbalancer-configuration,custom configuration>>.
890+
891+
We use delegates to work with `ServiceInstanceListSupplier` beans.
892+
We suggest passing a `DiscoveryClientServiceInstanceListSupplier` delegate in the constructor of `ZonePreferenceServiceInstanceListSupplier` and, in turn, wrapping the latter with a `CachingServiceInstanceListSupplier` to leverage <<loadbalancer-caching, LoadBalancer caching mechanism>>.
893+
894+
You could use this sample configuration to set it up:
895+
896+
[[zoned-based-custom-loadbalancer-configuration]]
897+
[source,java,indent=0]
898+
----
899+
public class CustomLoadBalancerConfiguration {
900+
901+
@Bean
902+
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
903+
ReactiveDiscoveryClient discoveryClient, Environment environment,
904+
ApplicationContext context) {
905+
DiscoveryClientServiceInstanceListSupplier firstDelegate = new DiscoveryClientServiceInstanceListSupplier(
906+
discoveryClient, environment);
907+
ZonePreferenceServiceInstanceListSupplier delegate = new ZonePreferenceServiceInstanceListSupplier(firstDelegate,
908+
environment);
909+
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
910+
.getBeanProvider(LoadBalancerCacheManager.class);
911+
if (cacheManagerProvider.getIfAvailable() != null) {
912+
return new CachingServiceInstanceListSupplier(delegate,
913+
cacheManagerProvider.getIfAvailable());
914+
}
915+
return delegate;
916+
}
917+
918+
}
919+
----
876920

877921
[[spring-cloud-loadbalancer-starter]]
878922
=== Spring Cloud LoadBalancer Starter
879923

880924
We also provide a starter that allows you to easily add Spring Cloud LoadBalancer in a Spring Boot app.
881-
In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your Spring
882-
Cloud dependencies in your build file.
925+
In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your Spring Cloud dependencies in your build file.
883926

884927
NOTE: Spring Cloud LoadBalancer starter includes
885928
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching]
886929
and https://github.com/stoyanr[Evictor].
887930

888-
WARNING: If you have both Ribbon and Spring Cloud LoadBalancer int the classpath, in order to maintain
889-
backward compatibility, Ribbon-based implementations will be used by default. In order
890-
to switch to using Spring Cloud LoadBalancer under the hood,
891-
make sure you set the property `spring.cloud.loadbalancer.ribbon.enabled` to `false`.
931+
WARNING: If you have both Ribbon and Spring Cloud LoadBalancer int the classpath, in order to maintain backward compatibility, Ribbon-based implementations will be used by default.
932+
In order to switch to using Spring Cloud LoadBalancer under the hood, make sure you set the property `spring.cloud.loadbalancer.ribbon.enabled` to `false`.
892933

934+
[[custom-loadbalancer-configuration]]
893935
=== Passing Your Own Spring Cloud LoadBalancer Configuration
894936

895937
You can also use the `@LoadBalancerClient` annotation to pass your own load-balancer client configuration, passing the name of the load-balancer client and the configuration class, as follows:
@@ -898,7 +940,7 @@ You can also use the `@LoadBalancerClient` annotation to pass your own load-bala
898940
[source,java,indent=0]
899941
----
900942
@Configuration
901-
@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class)
943+
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
902944
public class MyConfiguration {
903945
904946
@Bean
@@ -910,6 +952,13 @@ public class MyConfiguration {
910952
----
911953
====
912954

955+
You can use this feature to instantiate different implementations of `ServiceInstanceListSupplier` or `ReactorLoadBalancer`,
956+
either written by you, or provided by us as alternatives (for example `ZonePreferenceServiceInstanceListSupplier`) to override the default setup.
957+
958+
You can see an example of a custom cofiguration <<zoned-based-custom-loadbalancer-configuration,here>>.
959+
960+
NOTE: The annotation `value` arguments (`stores` in the example above) specifies the service id of the service that we should send the requests to with the given custom configuration.
961+
913962
You can also pass multiple configurations (for more than one load-balancer client) through the `@LoadBalancerClients` annotation, as the following example shows:
914963

915964
====
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.loadbalancer.core;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import reactor.core.publisher.Flux;
24+
25+
import org.springframework.cloud.client.ServiceInstance;
26+
import org.springframework.core.env.Environment;
27+
28+
/**
29+
* An implementation of {@link ServiceInstanceListSupplier} that filters instances
30+
* retrieved by the delegate by zone. The zone is retrieved from the
31+
* <code>spring.cloud.loadbalancer.zone</code> property. If the zone is not set or no instances are found for the
32+
* requested zone, all instances retrieved by the delegate are returned.
33+
*
34+
* @author Olga Maciaszek-Sharma
35+
* @since 2.2.1
36+
*/
37+
public class ZonePreferenceServiceInstanceListSupplier
38+
implements ServiceInstanceListSupplier {
39+
40+
private final String ZONE = "zone";
41+
42+
private final ServiceInstanceListSupplier delegate;
43+
44+
private final Environment environment;
45+
46+
private String zone;
47+
48+
public ZonePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
49+
Environment environment) {
50+
this.delegate = delegate;
51+
this.environment = environment;
52+
}
53+
54+
@Override
55+
public String getServiceId() {
56+
return delegate.getServiceId();
57+
}
58+
59+
@Override
60+
public Flux<List<ServiceInstance>> get() {
61+
return delegate.get().map(this::filteredByZone);
62+
}
63+
64+
private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
65+
if (zone == null) {
66+
zone = environment.getProperty("spring.cloud.loadbalancer.zone");
67+
}
68+
if (zone != null) {
69+
List<ServiceInstance> filteredInstances = new ArrayList<>();
70+
for (ServiceInstance serviceInstance : serviceInstances) {
71+
String instanceZone = getZone(serviceInstance);
72+
if (zone.equalsIgnoreCase(instanceZone)) {
73+
filteredInstances.add(serviceInstance);
74+
}
75+
}
76+
if (filteredInstances.size() > 0) {
77+
return filteredInstances;
78+
}
79+
}
80+
// If the zone is not set or there are no zone-specific instances available,
81+
// we return all instances retrieved for given service id.
82+
return serviceInstances;
83+
}
84+
85+
private String getZone(ServiceInstance serviceInstance) {
86+
Map<String, String> metadata = serviceInstance.getMetadata();
87+
if (metadata != null) {
88+
return metadata.get(ZONE);
89+
}
90+
return null;
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.loadbalancer.core;
18+
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.junit.jupiter.api.Test;
26+
import reactor.core.publisher.Flux;
27+
28+
import org.springframework.cloud.client.DefaultServiceInstance;
29+
import org.springframework.cloud.client.ServiceInstance;
30+
import org.springframework.core.env.Environment;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatCode;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
36+
37+
/**
38+
* Tests for {@link ZonePreferenceServiceInstanceListSupplier}.
39+
*
40+
* @author Olga Maciaszek-Sharma
41+
*/
42+
class ZonePreferenceServiceInstanceListSupplierTests {
43+
44+
private DiscoveryClientServiceInstanceListSupplier delegate = mock(
45+
DiscoveryClientServiceInstanceListSupplier.class);
46+
47+
private Environment environment = mock(Environment.class);
48+
49+
private ZonePreferenceServiceInstanceListSupplier supplier = new ZonePreferenceServiceInstanceListSupplier(
50+
delegate, environment);
51+
52+
private ServiceInstance first = serviceInstance("test-1", buildZoneMetadata("zone1"));
53+
54+
private ServiceInstance second = serviceInstance("test-2",
55+
buildZoneMetadata("zone1"));
56+
57+
private ServiceInstance third = serviceInstance("test-3", buildZoneMetadata("zone2"));
58+
59+
private ServiceInstance fourth = serviceInstance("test-4",
60+
buildZoneMetadata("zone3"));
61+
62+
private ServiceInstance fifth = serviceInstance("test-5", buildZoneMetadata(null));
63+
64+
@Test
65+
void shouldFilterInstancesByZone() {
66+
when(environment.getProperty("spring.cloud.loadbalancer.zone"))
67+
.thenReturn("zone1");
68+
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third, fourth, fifth)));
69+
70+
List<ServiceInstance> filtered = supplier.get().blockFirst();
71+
72+
assertThat(filtered).hasSize(2);
73+
assertThat(filtered).contains(first, second);
74+
assertThat(filtered).doesNotContain(third);
75+
assertThat(filtered).doesNotContain(fourth);
76+
assertThat(filtered).doesNotContain(fifth);
77+
}
78+
79+
@Test
80+
void shouldReturnAllInstancesIfNoZoneInstances() {
81+
when(environment.getProperty("spring.cloud.loadbalancer.zone"))
82+
.thenReturn("zone1");
83+
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(third, fourth)));
84+
85+
List<ServiceInstance> filtered = supplier.get().blockFirst();
86+
87+
assertThat(filtered).hasSize(2);
88+
assertThat(filtered).contains(third, fourth);
89+
}
90+
91+
@Test
92+
void shouldNotThrowNPEIfNullInstanceMetadata() {
93+
when(environment.getProperty("spring.cloud.loadbalancer.zone"))
94+
.thenReturn("zone1");
95+
when(delegate.get()).thenReturn(
96+
Flux.just(Collections.singletonList(serviceInstance("test-6", null))));
97+
assertThatCode(() -> supplier.get().blockFirst()).doesNotThrowAnyException();
98+
}
99+
100+
@Test
101+
void shouldReturnAllInstancesIfNoZone() {
102+
when(environment.getProperty("spring.cloud.loadbalancer.zone")).thenReturn(null);
103+
when(delegate.get())
104+
.thenReturn(Flux.just(Arrays.asList(first, second, third, fourth)));
105+
106+
List<ServiceInstance> filtered = supplier.get().blockFirst();
107+
108+
assertThat(filtered).hasSize(4);
109+
assertThat(filtered).contains(first, second, third, fourth);
110+
}
111+
112+
private DefaultServiceInstance serviceInstance(String instanceId,
113+
Map<String, String> metadata) {
114+
return new DefaultServiceInstance("test", instanceId, "http://test.test", 9080,
115+
false, metadata);
116+
}
117+
118+
private Map<String, String> buildZoneMetadata(String zone) {
119+
Map<String, String> metadata = new HashMap<>();
120+
metadata.put("zone", zone);
121+
return metadata;
122+
}
123+
124+
}

0 commit comments

Comments
 (0)