Add hint-based instance filtering (#923)
This commit is contained in:
committed by
GitHub
parent
6b6f6dbbd5
commit
272365490a
@@ -35,6 +35,7 @@
|
||||
|spring.cloud.loadbalancer.health-check.refetch-instances-interval | `25s` | Interval for refetching available service instances.
|
||||
|spring.cloud.loadbalancer.health-check.repeat-health-check | `true` | Indicates whether health checks should keep repeating. It might be useful to set it to <code>false</code> if periodically refetching the instances, as every refetch will also trigger a healthcheck.
|
||||
|spring.cloud.loadbalancer.hint | | Allows setting the value of <code>hint</code> that is passed on to the LoadBalancer request and can subsequently be used in {@link ReactiveLoadBalancer} implementations.
|
||||
|spring.cloud.loadbalancer.hint-header-name | `X-SC-LB-Hint` | Allows setting the name of the header used for passing the hint for hint-based service instance filtering.
|
||||
|spring.cloud.loadbalancer.retry.avoid-previous-instance | `true` | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
|
||||
|spring.cloud.loadbalancer.retry.backoff.enabled | `false` | Indicates whether Reactor Retry backoffs should be applied.
|
||||
|spring.cloud.loadbalancer.retry.backoff.jitter | `0.5` | Used to set {@link RetryBackoffSpec#jitter}.
|
||||
|
||||
@@ -932,7 +932,7 @@ public class CustomLoadBalancerConfiguration {
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
|
||||
ConfigurableApplicationContext context) {
|
||||
return ServiceInstanceListSuppliers.builder()
|
||||
return ServiceInstanceListSupplier.builder()
|
||||
.withDiscoveryClient()
|
||||
.withZonePreference()
|
||||
.withCaching()
|
||||
@@ -1053,6 +1053,38 @@ Spring Cloud LoadBalancer lets you set `String` hints that are passed to the Loa
|
||||
You can set a default hint for all services by setting the value of the `spring.cloud.loadbalancer.hint.default` property. You can also set a specific value
|
||||
for any given service by setting the value of the `spring.cloud.loadbalancer.hint.[SERVICE_ID]` property, substituting `[SERVICE_ID]` with the correct ID of your service. If the hint is not set by the user, `default` is used.
|
||||
|
||||
[[hints-based-loadbalancing]]
|
||||
=== Hint-Based Load-Balancing
|
||||
|
||||
We also provide a `HintBasedServiceInstanceListSupplier`, which is a `ServiceInstanceListSupplier` implementation for hint-based instance selection.
|
||||
|
||||
`HintBasedServiceInstanceListSupplier` checks for a hint request header (the default header-name is `X-SC-LB-Hint`, but you can modify it by changing the value of the `spring.cloud.loadbalancer.hint-header-name` property) and, if it finds a hint request header, uses the hint value passed in the header to filter service instances.
|
||||
|
||||
If no hint header has been added, `HintBasedServiceInstanceListSupplier` uses <<spring-cloud-loadbalancer-hints,hint values from properties>> to filter service instances.
|
||||
|
||||
If no hint is set, either by the header or by properties, all service instances provided by the delegate are returned.
|
||||
|
||||
While filtering, `HintBasedServiceInstanceListSupplier` looks for service instances that have a matching value set under the `hint` key in their `metadataMap`. If no matching instances are found, all instances provided by the delegate are returned.
|
||||
|
||||
You could use the following sample configuration to set it up:
|
||||
|
||||
[[hints-based-custom-loadbalancer-configuration]]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public class CustomLoadBalancerConfiguration {
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
|
||||
ConfigurableApplicationContext context) {
|
||||
return ServiceInstanceListSupplier.builder()
|
||||
.withDiscoveryClient()
|
||||
.withHints()
|
||||
.withCaching()
|
||||
.build(context);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[spring-cloud-loadbalancer-starter]]
|
||||
=== Spring Cloud LoadBalancer Starter
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ public class LoadBalancerProperties {
|
||||
*/
|
||||
private Map<String, String> hint = new LinkedCaseInsensitiveMap<>();
|
||||
|
||||
/**
|
||||
* Allows setting the name of the header used for passing the hint for hint-based
|
||||
* service instance filtering.
|
||||
*/
|
||||
private String hintHeaderName = "X-SC-LB-Hint";
|
||||
|
||||
/**
|
||||
* Properties for Spring-Retry and Reactor Retry support in Spring Cloud LoadBalancer.
|
||||
*/
|
||||
@@ -91,6 +97,14 @@ public class LoadBalancerProperties {
|
||||
this.stickySession = stickySession;
|
||||
}
|
||||
|
||||
public String getHintHeaderName() {
|
||||
return hintHeaderName;
|
||||
}
|
||||
|
||||
public void setHintHeaderName(String hintHeaderName) {
|
||||
this.hintHeaderName = hintHeaderName;
|
||||
}
|
||||
|
||||
public static class StickySession {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.loadbalancer.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.HintRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link ServiceInstanceListSupplier} implementation that uses hints to filter service
|
||||
* instances provided by the delegate.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.2
|
||||
*/
|
||||
public class HintBasedServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
public HintBasedServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
|
||||
LoadBalancerProperties properties) {
|
||||
super(delegate);
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<List<ServiceInstance>> get() {
|
||||
return delegate.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<List<ServiceInstance>> get(Request request) {
|
||||
return get().map(instances -> filteredByHint(instances, getHint(request.getContext())));
|
||||
}
|
||||
|
||||
private String getHint(Object requestContext) {
|
||||
if (requestContext == null) {
|
||||
return null;
|
||||
}
|
||||
String hint = null;
|
||||
if (requestContext instanceof RequestDataContext) {
|
||||
hint = getHintFromHeader((RequestDataContext) requestContext);
|
||||
}
|
||||
if (!StringUtils.hasText(hint) && requestContext instanceof HintRequestContext) {
|
||||
hint = ((HintRequestContext) requestContext).getHint();
|
||||
}
|
||||
return hint;
|
||||
}
|
||||
|
||||
private String getHintFromHeader(RequestDataContext context) {
|
||||
if (context.getClientRequest() != null) {
|
||||
HttpHeaders headers = context.getClientRequest().getHeaders();
|
||||
if (headers != null) {
|
||||
return headers.getFirst(properties.getHintHeaderName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<ServiceInstance> filteredByHint(List<ServiceInstance> instances, String hint) {
|
||||
if (!StringUtils.hasText(hint)) {
|
||||
return instances;
|
||||
}
|
||||
List<ServiceInstance> filteredInstances = new ArrayList<>();
|
||||
for (ServiceInstance serviceInstance : instances) {
|
||||
if (serviceInstance.getMetadata().getOrDefault("hint", "").equals(hint)) {
|
||||
filteredInstances.add(serviceInstance);
|
||||
}
|
||||
}
|
||||
if (filteredInstances.size() > 0) {
|
||||
return filteredInstances;
|
||||
}
|
||||
|
||||
// If instances cannot be found based on hint,
|
||||
// we return all instances retrieved for given service id.
|
||||
return instances;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,11 +48,6 @@ public class RequestBasedStickySessionServiceInstanceListSupplier extends Delega
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
return delegate.getServiceId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<List<ServiceInstance>> get() {
|
||||
return delegate.get();
|
||||
|
||||
@@ -239,6 +239,15 @@ public final class ServiceInstanceListSupplierBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServiceInstanceListSupplierBuilder withHints() {
|
||||
DelegateCreator creator = (context, delegate) -> {
|
||||
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
|
||||
return new HintBasedServiceInstanceListSupplier(delegate, properties);
|
||||
};
|
||||
creators.add(creator);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ServiceInstanceListSupplier} hierarchy.
|
||||
* @param context application context
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.loadbalancer.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestData;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link HintBasedServiceInstanceListSupplier}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class HintBasedServiceInstanceListSupplierTests {
|
||||
|
||||
private final DiscoveryClientServiceInstanceListSupplier delegate = mock(
|
||||
DiscoveryClientServiceInstanceListSupplier.class);
|
||||
|
||||
private final LoadBalancerProperties properties = new LoadBalancerProperties();
|
||||
|
||||
private final RequestDataContext requestContext = new RequestDataContext(
|
||||
new RequestData(new MockClientHttpRequest()));
|
||||
|
||||
private final HintBasedServiceInstanceListSupplier supplier = new HintBasedServiceInstanceListSupplier(delegate,
|
||||
properties);
|
||||
|
||||
private final ServiceInstance first = serviceInstance("test-1", buildHintMetadata("test1"));
|
||||
|
||||
private final ServiceInstance second = serviceInstance("test-2", buildHintMetadata("test2"));
|
||||
|
||||
private final ServiceInstance third = serviceInstance("test-3", new HashMap<>());
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties.setHintHeaderName("X-Test");
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnInstancesForHintFromHeaderWhenAvailable() {
|
||||
requestContext.setHint("test1");
|
||||
requestContext.getClientRequest().getHeaders().add("X-Test", "test2");
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered.get(0).getInstanceId()).isEqualTo("test-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnInstancesForHintFromPropertiesWhenNoHintHeader() {
|
||||
requestContext.setHint("test1");
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered.get(0).getInstanceId()).isEqualTo("test-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllInstancesWhenNoHint() {
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllInstancesWhenHintNotMatched() {
|
||||
requestContext.getClientRequest().getHeaders().add("X-Test", "testX");
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllInstancesWhenRequestContextNull() {
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(null);
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllInstancesWhenClientRequestNull() {
|
||||
Request<RequestDataContext> request = new DefaultRequest<>(new RequestDataContext(null));
|
||||
|
||||
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
|
||||
|
||||
assertThat(filtered).hasSize(3);
|
||||
}
|
||||
|
||||
private DefaultServiceInstance serviceInstance(String instanceId, Map<String, String> metadata) {
|
||||
return new DefaultServiceInstance(instanceId, "test", "http://test.test", 9080, false, metadata);
|
||||
}
|
||||
|
||||
private Map<String, String> buildHintMetadata(String zone) {
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("hint", zone);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user