From 23659f8aeafa419ee2f1ba30c3143c31934b2be5 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 19 Dec 2019 17:46:50 +0100 Subject: [PATCH] Gh 3718 add zoned loadbalancer instrumentation (#3720) * Add instrumentation for zoned LoadBalancer. * Add documentation. * Fix after review. * Fix after review. --- docs/src/main/asciidoc/_configprops.adoc | 1 + .../main/asciidoc/spring-cloud-netflix.adoc | 12 ++ spring-cloud-netflix-eureka-client/pom.xml | 5 + ...EurekaLoadBalancerClientConfiguration.java | 104 ++++++++++++++++++ .../EurekaLoadBalancerProperties.java | 44 ++++++++ .../LoadBalancerEurekaAutoConfiguration.java | 55 +++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...aLoadBalancerClientConfigurationTests.java | 75 +++++++++++++ 8 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfiguration.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerProperties.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/LoadBalancerEurekaAutoConfiguration.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfigurationTests.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index e97bf0fac..30851f6d9 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -19,6 +19,7 @@ |management.metrics.binders.hystrix.enabled | true | Enables creation of OK Http Client factory beans. |ribbon.eureka.enabled | true | Enables the use of Eureka with Ribbon. |spring.cloud.circuitbreaker.hystrix.enabled | true | Enables auto-configuration of the Hystrix Spring Cloud CircuitBreaker API implementation. +|spring.cloud.loadbalancer.eureka.approximate-zone-from-hostname | false | Used to determine whether we should try to get the `zone` value from host name. |spring.cloud.loadbalancer.ribbon.enabled | true | Causes `RibbonLoadBalancerClient` to be used by default. |zuul.ribbon-isolation-strategy | | |zuul.ribbon.eager-load.enabled | false | Enables eager loading of Ribbon clients on startup. diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 1a1cd3f98..b6361ecb3 100755 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -335,6 +335,18 @@ When a refresh occurs clients will be unregistered from the Eureka server and th where all instance of a given service are not available. One way to eliminate this from happening is to disable the ability to refresh Eureka clients. To do this set `eureka.client.refresh.enable=false`. +=== Using Eureka with Spring Cloud LoadBalancer + +We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`. +The `zone` value from the Eureka instance metadata (`eureka.instance.metadataMap.zone`) is used for setting the +value of `spring-clod-loadbalancer-zone` property that is used to filter service instances by zone. + +If that is missing and if the `spring.cloud.loadbalancer.eureka.approximateZoneFromHostname` flag is set to `true`, +it can use the domain name from the server hostname as a proxy for the zone. + +If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). +We take `eureka.client.availabilityZones`, which is a map from region name to a list of zones, and pull out the first zone for the instance's own region (that is, the `eureka.client.region`, which defaults to "us-east-1", for compatibility with native Netflix). + [[spring-cloud-eureka-server]] == Service Discovery: Eureka Server diff --git a/spring-cloud-netflix-eureka-client/pom.xml b/spring-cloud-netflix-eureka-client/pom.xml index 7a89d496c..e9f1337de 100644 --- a/spring-cloud-netflix-eureka-client/pom.xml +++ b/spring-cloud-netflix-eureka-client/pom.xml @@ -51,6 +51,11 @@ spring-cloud-config-server true + + org.springframework.cloud + spring-cloud-loadbalancer + true + com.netflix.eureka eureka-client diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfiguration.java new file mode 100644 index 000000000..36890dd65 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2019 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.netflix.eureka.loadbalancer; + +import javax.annotation.PostConstruct; + +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.discovery.EurekaClientConfig; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.netflix.ribbon.eureka.ZoneUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * A configuration for Spring Cloud LoadBalancer that retrieves client instance zone from + * Eureka and sets it as a property. Based on + * {@link EurekaLoadBalancerClientConfiguration}. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.1 + * @see EurekaLoadBalancerClientConfiguration + */ +@Configuration +@ConditionalOnBean({ LoadBalancerProperties.class, EurekaLoadBalancerProperties.class }) +public class EurekaLoadBalancerClientConfiguration { + + private static final String LOADBALANCER_ZONE = "spring.cloud.loadbalancer.zone"; + + private static final Log LOG = LogFactory + .getLog(EurekaLoadBalancerClientConfiguration.class); + + private final EurekaClientConfig clientConfig; + + private final EurekaInstanceConfig eurekaConfig; + + private final LoadBalancerProperties loadBalancerProperties; + + private final EurekaLoadBalancerProperties eurekaLoadBalancerProperties; + + public EurekaLoadBalancerClientConfiguration( + @Autowired(required = false) EurekaClientConfig clientConfig, + @Autowired(required = false) EurekaInstanceConfig eurekaInstanceConfig, + LoadBalancerProperties loadBalancerProperties, + EurekaLoadBalancerProperties eurekaLoadBalancerProperties) { + this.clientConfig = clientConfig; + this.eurekaConfig = eurekaInstanceConfig; + this.loadBalancerProperties = loadBalancerProperties; + this.eurekaLoadBalancerProperties = eurekaLoadBalancerProperties; + } + + @PostConstruct + public void postprocess() { + if (!StringUtils.isEmpty(loadBalancerProperties.getZone())) { + return; + } + String zone = getZoneFromEureka(); + if (!StringUtils.isEmpty(zone)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone); + } + loadBalancerProperties.setZone(zone); + } + } + + private String getZoneFromEureka() { + String zone; + boolean approximateZoneFromHostname = eurekaLoadBalancerProperties + .isApproximateZoneFromHostname(); + if (approximateZoneFromHostname && eurekaConfig != null) { + return ZoneUtils.extractApproximateZone(this.eurekaConfig.getHostName(false)); + } + else { + zone = eurekaConfig == null ? null + : eurekaConfig.getMetadataMap().get("zone"); + if (StringUtils.isEmpty(zone)) { + String[] zones = clientConfig + .getAvailabilityZones(clientConfig.getRegion()); + // Pick the first one from the regions we want to connect to + zone = zones != null && zones.length > 0 ? zones[0] : null; + } + return zone; + } + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerProperties.java new file mode 100644 index 000000000..be5355900 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 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.netflix.eureka.loadbalancer; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * A {@link ConfigurationProperties} bean for the Eureka-specific instrumentation of + * Spring Cloud LoadBalancer. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.1 + */ +@ConfigurationProperties("spring.cloud.loadbalancer.eureka") +public class EurekaLoadBalancerProperties { + + /** + * Used to determine whether we should try to get the `zone` value from host name. + */ + private boolean approximateZoneFromHostname = false; + + public boolean isApproximateZoneFromHostname() { + return approximateZoneFromHostname; + } + + public void setApproximateZoneFromHostname(boolean approximateZoneFromHostname) { + this.approximateZoneFromHostname = approximateZoneFromHostname; + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/LoadBalancerEurekaAutoConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/LoadBalancerEurekaAutoConfiguration.java new file mode 100644 index 000000000..6f9473568 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/loadbalancer/LoadBalancerEurekaAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2019 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.netflix.eureka.loadbalancer; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfigurationRegistrar; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * An Autoconfiguration that loads default config for Spring Cloud LoadBalancer clients. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.1 + * @see EurekaLoadBalancerClientConfiguration + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(EurekaLoadBalancerProperties.class) +@ConditionalOnClass(LoadBalancerClientConfigurationRegistrar.class) +@LoadBalancerClients(defaultConfiguration = EurekaLoadBalancerClientConfiguration.class) +public class LoadBalancerEurekaAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + EurekaLoadBalancerProperties eurekaLoadBalancerProperties() { + return new EurekaLoadBalancerProperties(); + } + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties("spring.cloud.loadbalancer") + LoadBalancerProperties loadBalancerProperties() { + return new LoadBalancerProperties(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories index 09ccf355e..12659725c 100644 --- a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories @@ -4,7 +4,8 @@ org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServi org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\ org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\ org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\ -org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration +org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\ +org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfigurationTests.java new file mode 100644 index 000000000..1eeff8827 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/loadbalancer/EurekaLoadBalancerClientConfigurationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2019 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.netflix.eureka.loadbalancer; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.InetUtilsProperties; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EurekaLoadBalancerClientConfiguration}. + * + * @author Olga Maciaszek-Sharma + */ +class EurekaLoadBalancerClientConfigurationTests { + + private EurekaClientConfigBean eurekaClientConfig = new EurekaClientConfigBean(); + + private EurekaInstanceConfigBean eurekaInstanceConfig = new EurekaInstanceConfigBean( + new InetUtils(new InetUtilsProperties())); + + private LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties(); + + private EurekaLoadBalancerProperties eurekaLoadBalancerProperties = new EurekaLoadBalancerProperties(); + + private EurekaLoadBalancerClientConfiguration postprocessor = new EurekaLoadBalancerClientConfiguration( + eurekaClientConfig, eurekaInstanceConfig, loadBalancerProperties, + eurekaLoadBalancerProperties); + + @Test + void shouldSetZoneFromInstanceMetadata() { + eurekaInstanceConfig.getMetadataMap().put("zone", "myZone"); + + postprocessor.postprocess(); + + assertThat(loadBalancerProperties.getZone()).isEqualTo("myZone"); + } + + @Test + public void shouldSetZoneToDefaultWhenNotSetInMetadata() { + postprocessor.postprocess(); + + assertThat(loadBalancerProperties.getZone()).isEqualTo("defaultZone"); + } + + @Test + public void shouldResolveApproximateZoneFromHost() { + eurekaInstanceConfig.setHostname("this.is.a.test.com"); + eurekaLoadBalancerProperties.setApproximateZoneFromHostname(true); + + postprocessor.postprocess(); + + assertThat(loadBalancerProperties.getZone()).isEqualTo("is.a.test.com"); + } + +}