Commit 997f5310 by Dave Syer

Make load balancer prefer the current zone if there is one

A EurekaInstanceConfig can declare a "zone" in its metadataMap, and then it will be matched by the RibbonLoadBalancerClient. The zone of a server is derived from the hostname - it is the whole host name or the host name excluding the least significant subdomain (period separated identifier). Fixes gh-29
parent c9dc8f51
...@@ -113,6 +113,10 @@ ...@@ -113,6 +113,10 @@
<artifactId>neo4j-cypher-compiler-2.1</artifactId> <artifactId>neo4j-cypher-compiler-2.1</artifactId>
<version>2.1.2</version> <version>2.1.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
package org.springframework.cloud.netflix.ribbon; package org.springframework.cloud.netflix.ribbon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration; import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.eureka.EurekaRibbonInitializer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.netflix.loadbalancer.BaseLoadBalancer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
...@@ -19,6 +24,12 @@ import java.util.List; ...@@ -19,6 +24,12 @@ import java.util.List;
@AutoConfigureAfter(EurekaClientAutoConfiguration.class) @AutoConfigureAfter(EurekaClientAutoConfiguration.class)
public class RibbonAutoConfiguration { public class RibbonAutoConfiguration {
@Autowired(required=false)
private List<BaseLoadBalancer> balancers = Collections.emptyList();
@Autowired(required=false)
private EurekaRibbonInitializer initializer;
@Bean @Bean
@ConditionalOnMissingBean(RestTemplate.class) @ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate restTemplate(RibbonInterceptor ribbonInterceptor) { public RestTemplate restTemplate(RibbonInterceptor ribbonInterceptor) {
...@@ -32,7 +43,7 @@ public class RibbonAutoConfiguration { ...@@ -32,7 +43,7 @@ public class RibbonAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoadBalancerClient.class) @ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() { public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(); return new RibbonLoadBalancerClient(balancers);
} }
@Bean @Bean
......
package org.springframework.cloud.netflix.ribbon; package org.springframework.cloud.netflix.ribbon;
import com.netflix.client.ClientFactory; import java.util.HashMap;
import com.netflix.loadbalancer.ILoadBalancer; import java.util.List;
import com.netflix.loadbalancer.Server; import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import com.netflix.client.ClientFactory;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer
*/ */
public class RibbonLoadBalancerClient implements LoadBalancerClient { public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Autowired @Autowired
private ServerListInitializer serverListInitializer; private ServerListInitializer serverListInitializer;
private Map<String, ILoadBalancer> balancers = new HashMap<String, ILoadBalancer>();
public RibbonLoadBalancerClient(List<BaseLoadBalancer> balancers) {
for (BaseLoadBalancer balancer : balancers) {
this.balancers.put(balancer.getName(), balancer);
}
}
@Override @Override
public ServiceInstance choose(String serviceId) { public ServiceInstance choose(String serviceId) {
serverListInitializer.initialize(serviceId); serverListInitializer.initialize(serviceId);
ILoadBalancer loadBalancer = ClientFactory.getNamedLoadBalancer(serviceId); ILoadBalancer loadBalancer = this.balancers.get(serviceId);
Server server = loadBalancer.chooseServer(null); if (loadBalancer == null) {
loadBalancer = ClientFactory.getNamedLoadBalancer(serviceId);
}
Server server = loadBalancer.chooseServer("default");
if (server == null) { if (server == null) {
throw new IllegalStateException("Unable to locate ILoadBalancer for service: "+ serviceId); throw new IllegalStateException(
"Unable to locate ILoadBalancer for service: " + serviceId);
} }
return new RibbonServer(serviceId, server); return new RibbonServer(serviceId, server);
} }
...@@ -46,7 +66,7 @@ public class RibbonLoadBalancerClient implements LoadBalancerClient { ...@@ -46,7 +66,7 @@ public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Override @Override
public String getIpAddress() { public String getIpAddress() {
return null; //TODO: ribbon doesn't supply ip return null; // TODO: ribbon doesn't supply ip
} }
@Override @Override
......
/*
* Copyright 2013-2014 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
*
* http://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.ribbon.eureka;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import org.springframework.util.StringUtils;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
/**
* @author Dave Syer
*
*/
public class DomainExtractingServerList implements ServerList<Server> {
private ServerList<Server> list;
public DomainExtractingServerList(ServerList<Server> list) {
this.list = list;
}
@Override
public List<Server> getInitialListOfServers() {
List<Server> servers = setZones(list.getInitialListOfServers());
return servers;
}
@Override
public List<Server> getUpdatedListOfServers() {
List<Server> servers = setZones(list.getUpdatedListOfServers());
return servers;
}
private List<Server> setZones(List<Server> servers) {
for (Server server : servers) {
if (!server.getZone().equals("default")) {
String zone = extractApproximateZone(server.getId());
server.setZone(zone);
}
}
return servers;
}
private String extractApproximateZone(String id) {
try {
URL url = new URL("http://" + id);
String host = url.getHost();
if (!host.contains(".")) {
return host;
}
String[] split = StringUtils.split(host, ".");
StringBuilder builder = new StringBuilder(split[1]);
for (int i=2; i<split.length; i++) {
builder.append(".").append(split[i]);
}
return builder.toString();
}
catch (MalformedURLException e) {
}
return "defaultZone";
}
}
package org.springframework.cloud.netflix.ribbon.eureka; package org.springframework.cloud.netflix.ribbon.eureka;
import com.netflix.config.ConfigurationManager; import static com.netflix.client.config.CommonClientConfigKey.DeploymentContextBasedVipAddresses;
import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList; import static com.netflix.client.config.CommonClientConfigKey.EnableZoneAffinity;
import static com.netflix.client.config.CommonClientConfigKey.NFLoadBalancerRuleClassName;
import static com.netflix.client.config.CommonClientConfigKey.NIWSServerListClassName;
import static com.netflix.client.config.CommonClientConfigKey.NIWSServerListFilterClassName;
import org.springframework.cloud.netflix.ribbon.ServerListInitializer; import org.springframework.cloud.netflix.ribbon.ServerListInitializer;
import static com.netflix.client.config.CommonClientConfigKey.*; import com.netflix.appinfo.AmazonInfo;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.client.ClientFactory;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DeploymentContext.ContextKey;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList;
/** /**
* Convenience class that sets up some configuration defaults for eureka-discovered ribbon
* clients.
*
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer
*/ */
public class EurekaRibbonInitializer implements ServerListInitializer { public class EurekaRibbonInitializer implements ServerListInitializer {
private EurekaInstanceConfig instance;
public EurekaRibbonInitializer(EurekaInstanceConfig instance) {
this.instance = instance;
}
@Override @Override
public void initialize(String serviceId) { public void initialize(String serviceId) {
//TODO: should this look more like hibernate spring boot props? if (instance != null
//TODO: only set the property if it hasn't already been set? && ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone) == null) {
setProp(serviceId, NIWSServerListClassName.key(), DiscoveryEnabledNIWSServerList.class.getName()); // You can set this with archaius.deployment.* (maybe requires
setProp(serviceId, DeploymentContextBasedVipAddresses.key(), serviceId); //FIXME: what should this be? // custom deployment context)?
String zone = instance.getMetadataMap().get("zone");
if (zone == null && instance.getDataCenterInfo() instanceof AmazonInfo) {
AmazonInfo info = (AmazonInfo) instance.getDataCenterInfo();
zone = info.getMetadata().get(AmazonInfo.MetaDataKey.availabilityZone);
}
if (zone != null) {
ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone,
zone);
}
}
// TODO: should this look more like hibernate spring boot props?
// TODO: only set the property if it hasn't already been set?
setProp(serviceId, NIWSServerListClassName.key(),
DiscoveryEnabledNIWSServerList.class.getName());
// FIXME: what should this be?
setProp(serviceId, DeploymentContextBasedVipAddresses.key(), serviceId);
setProp(serviceId, NFLoadBalancerRuleClassName.key(),
ZoneAvoidanceRule.class.getName());
setProp(serviceId, NIWSServerListFilterClassName.key(),
ZonePreferenceServerListFilter.class.getName());
setProp(serviceId, EnableZoneAffinity.key(), "true");
ILoadBalancer loadBalancer = ClientFactory.getNamedLoadBalancer(serviceId);
wrapServerList(loadBalancer);
}
private void wrapServerList(ILoadBalancer balancer) {
if (balancer instanceof DynamicServerListLoadBalancer) {
@SuppressWarnings("unchecked")
DynamicServerListLoadBalancer<Server> dynamic = (DynamicServerListLoadBalancer<Server>) balancer;
ServerList<Server> list = dynamic.getServerListImpl();
if (!(list instanceof DomainExtractingServerList)
&& !(instance.getDataCenterInfo() instanceof AmazonInfo)) {
// This is optional: you can use the native Eureka AWS features by making
// the EurekaInstanceConfig.dataCenterInfo an AmazonInfo
dynamic.setServerListImpl(new DomainExtractingServerList(list));
}
}
} }
protected void setProp(String serviceId, String suffix, String value) { protected void setProp(String serviceId, String suffix, String value) {
//how to set the namespace properly? // how to set the namespace properly?
String namespace = "ribbon"; String namespace = "ribbon";
ConfigurationManager.getConfigInstance().setProperty(serviceId + "."+ namespace +"." + suffix, value); ConfigurationManager.getConfigInstance().setProperty(
serviceId + "." + namespace + "." + suffix, value);
} }
} }
...@@ -15,7 +15,10 @@ ...@@ -15,7 +15,10 @@
*/ */
package org.springframework.cloud.netflix.ribbon.eureka; package org.springframework.cloud.netflix.ribbon.eureka;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList; import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
...@@ -36,8 +39,11 @@ import org.springframework.context.annotation.Configuration; ...@@ -36,8 +39,11 @@ import org.springframework.context.annotation.Configuration;
@AutoConfigureAfter(RibbonAutoConfiguration.class) @AutoConfigureAfter(RibbonAutoConfiguration.class)
public class RibbonEurekaAutoConfiguration { public class RibbonEurekaAutoConfiguration {
@Autowired(required=false)
private EurekaInstanceConfig instance;
@Bean @Bean
public ServerListInitializer serverListInitializer() { public ServerListInitializer serverListInitializer() {
return new EurekaRibbonInitializer(); return new EurekaRibbonInitializer(instance);
} }
} }
/*
* Copyright 2013-2014 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
*
* http://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.ribbon.eureka;
import java.util.ArrayList;
import java.util.List;
import com.netflix.client.config.IClientConfig;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DeploymentContext.ContextKey;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAffinityServerListFilter;
/**
* A filter that actively prefers the local zone (as defined by the deployment context, or
* the Eureka instance metadata).
*
* @author Dave Syer
*
*/
public class ZonePreferenceServerListFilter extends ZoneAffinityServerListFilter<Server> {
private String zone;
@Override
public void initWithNiwsConfig(IClientConfig niwsClientConfig) {
super.initWithNiwsConfig(niwsClientConfig);
if (ConfigurationManager.getDeploymentContext() != null) {
zone = ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone);
}
}
@Override
public List<Server> getFilteredListOfServers(List<Server> servers) {
List<Server> output = super.getFilteredListOfServers(servers);
if (zone != null && output.size() == servers.size()) {
List<Server> local = new ArrayList<Server>();
for (Server server : output) {
if (zone.equalsIgnoreCase(server.getZone())) {
local.add(server);
}
}
if (!local.isEmpty()) {
return local;
}
}
return output;
}
}
/*
* Copyright 2013-2014 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
*
* http://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.ribbon.eureka;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Test;
import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean;
import com.netflix.client.ClientFactory;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DeploymentContext.ContextKey;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAwareLoadBalancer;
/**
* @author Dave Syer
*
*/
public class EurekaRibbonInitializerTests {
@After
public void close() {
ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone, "");
}
@Test
public void basicConfigurationCreatedForLoadBalancer() {
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean();
instance.getMetadataMap().put("zone", "foo");
EurekaRibbonInitializer initializer = new EurekaRibbonInitializer(
instance);
initializer.initialize("service");
ILoadBalancer balancer = ClientFactory.getNamedLoadBalancer("service");
assertNotNull(balancer);
@SuppressWarnings("unchecked")
ZoneAwareLoadBalancer<Server> aware = (ZoneAwareLoadBalancer<Server>) balancer;
assertTrue(aware.getServerListImpl() instanceof DomainExtractingServerList);
assertEquals("foo", ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone));
}
}
/*
* Copyright 2013-2014 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
*
* http://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.ribbon.eureka;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.netflix.loadbalancer.Server;
/**
* @author Dave Syer
*
*/
public class ZonePreferenceServerListFilterTests {
private Server dsyer = new Server("dsyer", 8080);
private Server localhost = new Server("localhost", 8080);
@Before
public void init() {
dsyer.setZone("dsyer");
localhost.setZone("localhost");
}
@Test
public void noZoneSet() {
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
List<Server> result = filter.getFilteredListOfServers(Arrays.asList(localhost));
assertEquals(1, result.size());
}
@Test
public void withZoneSetAndNoMatches() {
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
ReflectionTestUtils.setField(filter, "zone", "dsyer");
List<Server> result = filter.getFilteredListOfServers(Arrays.asList(localhost));
assertEquals(1, result.size());
}
@Test
public void withZoneSetAndMatches() {
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
ReflectionTestUtils.setField(filter, "zone", "dsyer");
List<Server> result = filter.getFilteredListOfServers(Arrays.asList(dsyer, localhost));
assertEquals(1, result.size());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment