feat(lb): deterministic subsetting algorithm (#1289)

This commit is contained in:
纪卓志
2023-11-14 01:00:23 +08:00
committed by GitHub
parent c6249fe0db
commit cefe2b55a2
6 changed files with 382 additions and 0 deletions

View File

@@ -143,6 +143,15 @@ public class LoadBalancerClientConfiguration {
.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)
@@ -208,6 +217,15 @@ public class LoadBalancerClientConfiguration {
.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)
@@ -353,4 +371,14 @@ public class LoadBalancerClientConfiguration {
}
static class SubsetConfigurationCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return LoadBalancerEnvironmentPropertyUtils.equalToForClientOrDefault(context.getEnvironment(),
"configurations", "subset");
}
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
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;
@@ -301,6 +302,16 @@ public final class ServiceInstanceListSupplierBuilder {
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.

View File

@@ -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;
}
}

View File

@@ -0,0 +1,174 @@
/*
* 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.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.jupiter.api.AfterEach;
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.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.commons.util.IdUtils;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.loadbalancer.core.LoadBalancerTestUtils.buildLoadBalancerClientFactory;
/**
* Tests for {@link SubsetServiceInstanceListSupplier}
*
* @author Zhuozhi Ji
*/
class SubsetServiceInstanceListSupplierTest {
private final DiscoveryClientServiceInstanceListSupplier delegate = mock(
DiscoveryClientServiceInstanceListSupplier.class);
private MockEnvironment env;
@BeforeEach
public void setup() {
env = new MockEnvironment();
}
@AfterEach
public void destroy() {
env = null;
}
@Test
void shouldResolvePlaceholderWhenInstanceIdSet() {
env.setProperty("foo", "bar");
when(delegate.getServiceId()).thenReturn("test");
SubsetServiceInstanceListSupplier supplier = new SubsetServiceInstanceListSupplier(delegate, env,
factory("${foo}", 100));
assertThat(supplier.getInstanceId()).isEqualTo("bar");
}
@Test
void shouldUseIdUtilsWhenInstanceIdNotSet() {
SubsetServiceInstanceListSupplier supplier = new SubsetServiceInstanceListSupplier(delegate, env,
factory("", 100));
when(delegate.getServiceId()).thenReturn("test");
assertThat(supplier.getInstanceId()).isEqualTo(IdUtils.getDefaultInstanceId(env));
}
@Test
void shouldReturnEmptyWhenDelegateReturnedEmpty() {
when(delegate.getServiceId()).thenReturn("test");
when(delegate.get()).thenReturn(Flux.just(Collections.emptyList()));
SubsetServiceInstanceListSupplier supplier = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar", 100));
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst());
assertThat(serviceInstances).isEmpty();
}
@Test
void shouldReturnSublistWithGivenSubsetSize() {
List<ServiceInstance> instances = IntStream.range(0, 101)
.mapToObj(i -> new DefaultServiceInstance(Integer.toString(i), "test", "host" + i, 8080, false, null))
.collect(Collectors.toList());
when(delegate.getServiceId()).thenReturn("test");
when(delegate.get()).thenReturn(Flux.just(instances));
SubsetServiceInstanceListSupplier supplier = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar", 5));
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst());
assertThat(serviceInstances).hasSize(5);
}
@Test
void shouldReturnRawWhenLessThanSubsetSize() {
List<ServiceInstance> instances = IntStream.range(0, 101)
.mapToObj(i -> new DefaultServiceInstance(Integer.toString(i), "test", "host" + i, 8080, false, null))
.collect(Collectors.toList());
when(delegate.getServiceId()).thenReturn("test");
when(delegate.get()).thenReturn(Flux.just(instances));
SubsetServiceInstanceListSupplier supplier = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar", 1000));
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst());
assertThat(serviceInstances).hasSize(101);
}
@Test
void shouldReturnSameSublistForSameInstanceId() {
List<ServiceInstance> instances = IntStream.range(0, 101)
.mapToObj(i -> new DefaultServiceInstance(Integer.toString(i), "test", "host" + i, 8080, false, null))
.collect(Collectors.toList());
when(delegate.getServiceId()).thenReturn("test");
when(delegate.get()).thenReturn(Flux.just(instances));
SubsetServiceInstanceListSupplier supplier1 = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar", 5));
List<ServiceInstance> serviceInstances1 = Objects.requireNonNull(supplier1.get().blockFirst());
SubsetServiceInstanceListSupplier supplier2 = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar", 5));
List<ServiceInstance> serviceInstances2 = Objects.requireNonNull(supplier2.get().blockFirst());
assertThat(serviceInstances1).isEqualTo(serviceInstances2);
}
@Test
void shouldReturnDifferentSublistForDifferentInstanceId() {
List<ServiceInstance> instances = IntStream.range(0, 101)
.mapToObj(i -> new DefaultServiceInstance(Integer.toString(i), "test", "host" + i, 8080, false, null))
.collect(Collectors.toList());
when(delegate.getServiceId()).thenReturn("test");
when(delegate.get()).thenReturn(Flux.just(instances));
SubsetServiceInstanceListSupplier supplier1 = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar1", 5));
List<ServiceInstance> serviceInstances1 = Objects.requireNonNull(supplier1.get().blockFirst());
SubsetServiceInstanceListSupplier supplier2 = new SubsetServiceInstanceListSupplier(delegate, env,
factory("foobar2", 5));
List<ServiceInstance> serviceInstances2 = Objects.requireNonNull(supplier2.get().blockFirst());
assertThat(serviceInstances1).isNotEqualTo(serviceInstances2);
}
ReactiveLoadBalancer.Factory<ServiceInstance> factory(String instanceId, int size) {
LoadBalancerProperties properties = new LoadBalancerProperties();
LoadBalancerProperties.Subset subset = new LoadBalancerProperties.Subset();
subset.setInstanceId(instanceId);
subset.setSize(size);
properties.setSubset(subset);
return buildLoadBalancerClientFactory("test", properties);
}
}