Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature 2022.x: Expand NacosLoadBalancer to conveniently support custom service list filtering and load balancing algorithms #3801

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2013-2023 the original author or authors.
yuluo-yx marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 com.alibaba.cloud.nacos.loadbalancer;

import java.util.List;

import com.alibaba.cloud.nacos.balancer.NacosBalancer;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.core.Ordered;

/**
* This is a default implementation of load balancing algorithm.
* use {@link NacosBalancer}
*
* @author <a href="mailto:[email protected]">zhangbinhub</a>
*/
public class DefaultLoadBalancerAlgorithm implements LoadBalancerAlgorithm {
@Override
public String getServiceId() {
return LoadBalancerAlgorithm.DEFAULT_SERVICE_ID;
}

@Override
public ServiceInstance getInstance(Request<?> request, List<ServiceInstance> serviceInstances) {
return NacosBalancer.getHostByRandomWeight3(serviceInstances);
}

@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2013-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 com.alibaba.cloud.nacos.loadbalancer;

import java.util.List;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.core.Ordered;

/**
* Load Balancer algorithm interface.
* When expanding the load balancing algorithm, implement this interface and register it as a bean.
*
* @author <a href="mailto:[email protected]">zhangbinhub</a>
*/
public interface LoadBalancerAlgorithm extends Ordered {
/**
* default service id.
*/
String DEFAULT_SERVICE_ID = "defaultServiceId";

String getServiceId();

ServiceInstance getInstance(Request<?> request, List<ServiceInstance> serviceInstances);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
Expand All @@ -32,5 +33,8 @@
@ConditionalOnNacosDiscoveryEnabled
@LoadBalancerClients(defaultConfiguration = NacosLoadBalancerClientConfiguration.class)
public class LoadBalancerNacosAutoConfiguration {

@Bean
public LoadBalancerAlgorithm defaultLoadBalancerAlgorithm() {
return new DefaultLoadBalancerAlgorithm();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
import com.alibaba.cloud.nacos.util.InetIPv6Utils;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import jakarta.annotation.PostConstruct;
Expand All @@ -32,7 +32,6 @@
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
Expand All @@ -55,7 +54,7 @@ public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {

private final String serviceId;

private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

private final NacosDiscoveryProperties nacosDiscoveryProperties;

Expand All @@ -67,9 +66,11 @@ public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {
*/
public static String ipv6;

@Autowired
private InetIPv6Utils inetIPv6Utils;
private final InetIPv6Utils inetIPv6Utils;

private final List<ServiceInstanceFilter> serviceInstanceFilters;

private final Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap;

@PostConstruct
public void init() {
Expand All @@ -96,7 +97,7 @@ private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> insta
}
}
// Provider has no IPv6, should use IPv4.
if (ipv6InstanceList.size() == 0) {
if (ipv6InstanceList.isEmpty()) {
return instances.stream()
.filter(instance -> Pattern.matches(IPV4_REGEX, instance.getHost()))
.collect(Collectors.toList());
Expand All @@ -112,23 +113,28 @@ private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> insta

public NacosLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties, InetIPv6Utils inetIPv6Utils,
List<ServiceInstanceFilter> serviceInstanceFilters,
Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.inetIPv6Utils = inetIPv6Utils;
this.serviceInstanceFilters = serviceInstanceFilters;
this.loadBalancerAlgorithmMap = loadBalancerAlgorithmMap;
}

@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(this::getInstanceResponse);
return supplier.get(request).next().map(serviceInstances -> getInstanceResponse(request, serviceInstances));
}

private Response<ServiceInstance> getInstanceResponse(
private Response<ServiceInstance> getInstanceResponse(Request<?> request,
List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
log.warn("No servers available for service: {}", this.serviceId);
return new EmptyResponse();
}

Expand All @@ -143,20 +149,31 @@ private Response<ServiceInstance> getInstanceResponse(
.get("nacos.cluster");
return StringUtils.equals(cluster, clusterName);
}).collect(Collectors.toList());
if (CollectionUtils.isEmpty(sameClusterInstances)) {
log.warn("Not filtering to the specified cluster instance node,name = {}, clusterName = {}, instance = {}",
serviceId, clusterName, serviceInstances);
}
else {
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
log.info("Calling to filter cluster instance nodes, name = {}, clusterName = {}, instance = {}",
serviceId, clusterName, serviceInstances);
}
}
else {
log.warn(
"A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
serviceId, clusterName, serviceInstances);
}
instancesToChoose = this.filterInstanceByIpType(instancesToChoose);

ServiceInstance instance = NacosBalancer
.getHostByRandomWeight3(instancesToChoose);
// Filter the service list sequentially based on the order number
for (ServiceInstanceFilter filter : serviceInstanceFilters) {
instancesToChoose = filter.filterInstance(request, instancesToChoose);
}

ServiceInstance instance;
// Find the corresponding load balancing algorithm through the service ID and select the final service instance
if (loadBalancerAlgorithmMap.containsKey(serviceId)) {
instance = loadBalancerAlgorithmMap.get(serviceId).getInstance(request, instancesToChoose);
}
else {
instance = loadBalancerAlgorithmMap.get(LoadBalancerAlgorithm.DEFAULT_SERVICE_ID)
.getInstance(request, instancesToChoose);
}

return new DefaultResponse(instance);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@

package com.alibaba.cloud.nacos.loadbalancer;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.util.InetIPv6Utils;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
Expand Down Expand Up @@ -47,6 +52,7 @@
* @since 2021.1
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnLoadBalancerNacos
@ConditionalOnDiscoveryEnabled
public class NacosLoadBalancerClientConfiguration {

Expand All @@ -56,12 +62,22 @@ public class NacosLoadBalancerClientConfiguration {
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory,
NacosDiscoveryProperties nacosDiscoveryProperties) {
NacosDiscoveryProperties nacosDiscoveryProperties,
InetIPv6Utils inetIPv6Utils,
List<ServiceInstanceFilter> serviceInstanceFilters,
List<LoadBalancerAlgorithm> loadBalancerAlgorithms) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap = new HashMap<>();
loadBalancerAlgorithms.forEach(loadBalancerAlgorithm -> {
if (!loadBalancerAlgorithmMap.containsKey(loadBalancerAlgorithm.getServiceId())) {
loadBalancerAlgorithmMap.put(loadBalancerAlgorithm.getServiceId(), loadBalancerAlgorithm);
}
});
return new NacosLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class),
name, nacosDiscoveryProperties);
name, nacosDiscoveryProperties, inetIPv6Utils,
serviceInstanceFilters, loadBalancerAlgorithmMap);
}

@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -115,7 +131,5 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient()
.withZonePreference().build(context);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2013-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 com.alibaba.cloud.nacos.loadbalancer;

import java.util.List;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.core.Ordered;

/**
* Service Instance Filter interface.
* When custom service instance list filter, implement this interface and register it as a bean.
*
* @author <a href="mailto:[email protected]">zhangbinhub</a>
*/
public interface ServiceInstanceFilter extends Ordered {
List<ServiceInstance> filterInstance(Request<?> request, List<ServiceInstance> serviceInstances);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2013-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 com.alibaba.cloud.nacos.discovery;

import com.alibaba.cloud.nacos.NacosServiceAutoConfiguration;
import com.alibaba.cloud.nacos.loadbalancer.LoadBalancerAlgorithm;
import com.alibaba.cloud.nacos.loadbalancer.LoadBalancerNacosAutoConfiguration;
import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration;
import com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration;
import com.alibaba.cloud.nacos.util.UtilIPv6AutoConfiguration;
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration;
import org.springframework.cloud.commons.util.UtilAutoConfiguration;
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;

import static org.assertj.core.api.Assertions.assertThat;

/**
* @author <a href="mailto:[email protected]">zhangbinhub</a>
**/
public class NacosDiscoveryLoadBalancerConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
AutoServiceRegistrationConfiguration.class,
NacosServiceRegistryAutoConfiguration.class,
UtilAutoConfiguration.class,
UtilIPv6AutoConfiguration.class,
NacosServiceAutoConfiguration.class,
NacosDiscoveryAutoConfiguration.class,
NacosDiscoveryClientConfiguration.class,
LoadBalancerAutoConfiguration.class, this.getClass()));

@Test
public void testNacosLoadBalancerEnabled() {
contextRunner.withPropertyValues("spring.cloud.loadbalancer.nacos.enabled=true")
.withConfiguration(AutoConfigurations.of(
LoadBalancerNacosAutoConfiguration.class,
NacosLoadBalancerClientConfiguration.class))
.run(context -> {
assertThat(context).hasSingleBean(LoadBalancerAlgorithm.class);
assertThat(context).hasBean("nacosLoadBalancer");
});
}

@Test
public void testNacosLoadBalancerDisabled() {
contextRunner.withPropertyValues("spring.cloud.loadbalancer.nacos.enabled=false")
.withConfiguration(AutoConfigurations.of(
LoadBalancerNacosAutoConfiguration.class,
NacosLoadBalancerClientConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(LoadBalancerAlgorithm.class);
assertThat(context).doesNotHaveBean("nacosLoadBalancer");
});
}

}
Loading