From e9ba0afd4ca29a66722f0131dc2a91ac321ab17e Mon Sep 17 00:00:00 2001 From: ZhangBin Date: Tue, 16 Jul 2024 17:08:29 +0800 Subject: [PATCH] feature 2023.x: Expand NacosLoadBalancer to conveniently support custom service list filtering and load balancing algorithms (#3794) --- .../DefaultLoadBalancerAlgorithm.java | 48 ++++++++++++ .../loadbalancer/LoadBalancerAlgorithm.java | 40 ++++++++++ .../LoadBalancerNacosAutoConfiguration.java | 6 +- .../nacos/loadbalancer/NacosLoadBalancer.java | 42 ++++++++--- .../NacosLoadBalancerClientConfiguration.java | 22 +++++- .../loadbalancer/ServiceInstanceFilter.java | 33 ++++++++ ...iscoveryLoadBalancerConfigurationTest.java | 75 +++++++++++++++++++ 7 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/DefaultLoadBalancerAlgorithm.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerAlgorithm.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/ServiceInstanceFilter.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryLoadBalancerConfigurationTest.java diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/DefaultLoadBalancerAlgorithm.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/DefaultLoadBalancerAlgorithm.java new file mode 100644 index 0000000000..43d4c1cc71 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/DefaultLoadBalancerAlgorithm.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023-2024 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 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 com.alibaba.cloud.nacos.balancer.NacosBalancer} + * + * @author zhangbinhub + */ +public class DefaultLoadBalancerAlgorithm implements LoadBalancerAlgorithm { + @Override + public String getServiceId() { + return LoadBalancerAlgorithm.DEFAULT_SERVICE_ID; + } + + @Override + public ServiceInstance getInstance(Request request, List serviceInstances) { + return NacosBalancer.getHostByRandomWeight3(serviceInstances); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerAlgorithm.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerAlgorithm.java new file mode 100644 index 0000000000..759ccc093d --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerAlgorithm.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023-2024 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 zhangbinhub + */ +public interface LoadBalancerAlgorithm extends Ordered { + /** + * default service id. + */ + String DEFAULT_SERVICE_ID = "defaultServiceId"; + + String getServiceId(); + + ServiceInstance getInstance(Request request, List serviceInstances); +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerNacosAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerNacosAutoConfiguration.java index e600e33c11..a185511999 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerNacosAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/LoadBalancerNacosAutoConfiguration.java @@ -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; /** @@ -32,5 +33,8 @@ @ConditionalOnNacosDiscoveryEnabled @LoadBalancerClients(defaultConfiguration = NacosLoadBalancerClientConfiguration.class) public class LoadBalancerNacosAutoConfiguration { - + @Bean + public LoadBalancerAlgorithm defaultLoadBalancerAlgorithm() { + return new DefaultLoadBalancerAlgorithm(); + } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancer.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancer.java index 733a62dc5c..6856072472 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancer.java +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancer.java @@ -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; @@ -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; @@ -55,7 +54,7 @@ public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final String serviceId; - private ObjectProvider serviceInstanceListSupplierProvider; + private final ObjectProvider serviceInstanceListSupplierProvider; private final NacosDiscoveryProperties nacosDiscoveryProperties; @@ -67,9 +66,11 @@ public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer { */ public static String ipv6; - @Autowired - private InetIPv6Utils inetIPv6Utils; + private final InetIPv6Utils inetIPv6Utils; + private final List serviceInstanceFilters; + + private final Map loadBalancerAlgorithmMap; @PostConstruct public void init() { @@ -96,7 +97,7 @@ private List filterInstanceByIpType(List 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()); @@ -112,23 +113,28 @@ private List filterInstanceByIpType(List insta public NacosLoadBalancer( ObjectProvider serviceInstanceListSupplierProvider, - String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) { + String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties, InetIPv6Utils inetIPv6Utils, + List serviceInstanceFilters, + Map loadBalancerAlgorithmMap) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; this.nacosDiscoveryProperties = nacosDiscoveryProperties; + this.inetIPv6Utils = inetIPv6Utils; + this.serviceInstanceFilters = serviceInstanceFilters; + this.loadBalancerAlgorithmMap = loadBalancerAlgorithmMap; } @Override public Mono> 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 getInstanceResponse( + private Response getInstanceResponse(Request request, List 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(); } @@ -154,8 +160,20 @@ private Response getInstanceResponse( } 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); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancerClientConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancerClientConfiguration.java index 494e7e96ad..c3942cda0b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancerClientConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/NacosLoadBalancerClientConfiguration.java @@ -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; @@ -47,6 +52,7 @@ * @since 2021.1 */ @Configuration(proxyBeanMethods = false) +@ConditionalOnLoadBalancerNacos @ConditionalOnDiscoveryEnabled public class NacosLoadBalancerClientConfiguration { @@ -56,12 +62,22 @@ public class NacosLoadBalancerClientConfiguration { @ConditionalOnMissingBean public ReactorLoadBalancer nacosLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory, - NacosDiscoveryProperties nacosDiscoveryProperties) { + NacosDiscoveryProperties nacosDiscoveryProperties, + InetIPv6Utils inetIPv6Utils, + List serviceInstanceFilters, + List loadBalancerAlgorithms) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + Map 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) @@ -115,7 +131,5 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient() .withZonePreference().build(context); } - } - } diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/ServiceInstanceFilter.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/ServiceInstanceFilter.java new file mode 100644 index 0000000000..4f3dcc3fd9 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/main/java/com/alibaba/cloud/nacos/loadbalancer/ServiceInstanceFilter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023-2024 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 zhangbinhub + */ +public interface ServiceInstanceFilter extends Ordered { + List filterInstance(Request request, List serviceInstances); +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryLoadBalancerConfigurationTest.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryLoadBalancerConfigurationTest.java new file mode 100644 index 0000000000..8c5cb9f56e --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-discovery/src/test/java/com/alibaba/cloud/nacos/discovery/NacosDiscoveryLoadBalancerConfigurationTest.java @@ -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 zhangbinhub + **/ +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"); + }); + } + +}