Skip to content

Commit

Permalink
feat(lb): deterministic subsetting algorithm (#1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
jizhuozhi authored Nov 13, 2023
1 parent c6249fe commit cefe2b5
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 0 deletions.
28 changes: 28 additions & 0 deletions docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,34 @@ For `WebClient`, you need to implement and define `LoadBalancerClientRequestTran
If multiple transformers are defined, they are applied in the order in which Beans are defined.
Alternatively, you can use `LoadBalancerRequestTransformer.DEFAULT_ORDER` or `LoadBalancerClientRequestTransformer.DEFAULT_ORDER` to specify the order.

[[loadbalancer-subset]]
== Spring Cloud LoadBalancer Subset

`SubsetServiceInstanceListSupplier` implements a https://sre.google/sre-book/load-balancing-datacenter/[deterministic subsetting algorithm] to select a limited number of instances in the `ServiceInstanceListSupplier` delegates hierarchy.

You can configure it either by setting the value of `spring.cloud.loadbalancer.configurations` to `subset` or by providing your own `ServiceInstanceListSupplier` bean -- for example:

[[subset-custom-loadbalancer-configuration-example]]
[source,java,indent=0]
----
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withSubset()
.withCaching()
.build(context);
}
}
----

TIP: By default, each service instance is assigned a unique `instanceId`, and different `instanceId` values often select different subsets. Normally, you need not pay attention to it. However, if you need to have multiple instances select the same subset, you can set it with `spring.cloud.loadbalancer.subset.instance-id` (which supports placeholders).

TIP: By default, the size of the subset is set to 100. You can also set it with `spring.cloud.loadbalancer.subset.size`.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.commons.util.IdUtils;
import org.springframework.core.env.PropertyResolver;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedCaseInsensitiveMap;

Expand All @@ -40,6 +42,7 @@
*
* @author Olga Maciaszek-Sharma
* @author Gandhimathi Velusamy
* @author Zhuozhi Ji
* @since 2.2.1
*/
public class LoadBalancerProperties {
Expand Down Expand Up @@ -85,6 +88,12 @@ public class LoadBalancerProperties {
*/
private boolean callGetWithRequestOnDelegates = true;

/**
* Properties for
* {@link org.springframework.cloud.loadbalancer.core.SubsetServiceInstanceListSupplier}.
*/
private Subset subset = new Subset();

public HealthCheck getHealthCheck() {
return healthCheck;
}
Expand Down Expand Up @@ -142,6 +151,14 @@ public boolean isCallGetWithRequestOnDelegates() {
return callGetWithRequestOnDelegates;
}

public Subset getSubset() {
return subset;
}

public void setSubset(Subset subset) {
this.subset = subset;
}

public void setCallGetWithRequestOnDelegates(boolean callGetWithRequestOnDelegates) {
this.callGetWithRequestOnDelegates = callGetWithRequestOnDelegates;
}
Expand Down Expand Up @@ -490,4 +507,35 @@ public void setEnabled(boolean enabled) {

}

public static class Subset {

/**
* Instance id of deterministic subsetting. If not set,
* {@link IdUtils#getDefaultInstanceId(PropertyResolver)} will be used.
*/
private String instanceId = "";

/**
* Max subset size of deterministic subsetting.
*/
private int size = 100;

public String getInstanceId() {
return instanceId;
}

public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}

public int getSize() {
return size;
}

public void setSize(int size) {
this.size = size;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ public ServiceInstanceListSupplier weightedServiceInstanceListSupplier(Configura
.build(context);
}

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(SubsetConfigurationCondition.class)
public ServiceInstanceListSupplier subsetServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withSubset().withCaching()
.build(context);
}

}

@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -208,6 +217,15 @@ public ServiceInstanceListSupplier weightedServiceInstanceListSupplier(Configura
.build(context);
}

@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(SubsetConfigurationCondition.class)
public ServiceInstanceListSupplier subsetServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withSubset().withCaching()
.build(context);
}

}

@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -353,4 +371,14 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)

}

static class SubsetConfigurationCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return LoadBalancerEnvironmentPropertyUtils.equalToForClientOrDefault(context.getEnvironment(),
"configurations", "subset");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.PropertyResolver;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -301,6 +302,16 @@ public ServiceInstanceListSupplierBuilder withHints() {
return this;
}

public ServiceInstanceListSupplierBuilder withSubset() {
DelegateCreator creator = (context, delegate) -> {
PropertyResolver resolver = context.getBean(PropertyResolver.class);
LoadBalancerClientFactory factory = context.getBean(LoadBalancerClientFactory.class);
return new SubsetServiceInstanceListSupplier(delegate, resolver, factory);
};
creators.add(creator);
return this;
}

/**
* Support {@link ServiceInstanceListSupplierBuilder} can be added to the expansion
* implementation of {@link ServiceInstanceListSupplier} by this method.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2012-2023 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.Collections;
import java.util.List;
import java.util.Random;

import reactor.core.publisher.Flux;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.commons.util.IdUtils;
import org.springframework.core.env.PropertyResolver;
import org.springframework.util.StringUtils;

/**
* A {@link ServiceInstanceListSupplier} implementation that uses
* <a href="https://sre.google/sre-book/load-balancing-datacenter/">deterministic
* subsetting algorithm</a> to limit the number of instances provided by delegate.
*
* @author Zhuozhi Ji
* @since 4.1.0
*/
public class SubsetServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {

private final String instanceId;

private final int size;

public SubsetServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, PropertyResolver resolver,
ReactiveLoadBalancer.Factory<ServiceInstance> factory) {
super(delegate);
LoadBalancerProperties properties = factory.getProperties(getServiceId());
this.instanceId = resolveInstanceId(properties, resolver);
this.size = properties.getSubset().getSize();
}

@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(instances -> {
if (instances.size() <= size) {
return instances;
}

instances = new ArrayList<>(instances);

int instanceId = this.instanceId.hashCode() & Integer.MAX_VALUE;
int count = instances.size() / size;
int round = instanceId / count;

Random random = new Random(round);
Collections.shuffle(instances, random);

int bucket = instanceId % count;
int start = bucket * size;
return instances.subList(start, start + size);
});
}

private static String resolveInstanceId(LoadBalancerProperties properties, PropertyResolver resolver) {
String instanceId = properties.getSubset().getInstanceId();
if (StringUtils.hasText(instanceId)) {
return resolver.resolvePlaceholders(properties.getSubset().getInstanceId());
}
return IdUtils.getDefaultInstanceId(resolver);
}

public String getInstanceId() {
return instanceId;
}

public int getSize() {
return size;
}

}
Loading

0 comments on commit cefe2b5

Please sign in to comment.