Commit 2ec0a00f by Nepxion

增加基于随机权重的平滑灰度发布功能

parent 7ad6f973
......@@ -13,6 +13,7 @@ public class DiscoveryEntity extends FilterHolderEntity {
private static final long serialVersionUID = -7417362859952278987L;
private VersionFilterEntity versionFilterEntity;
private WeightFilterEntity weightFilterEntity;
public DiscoveryEntity() {
......@@ -25,4 +26,12 @@ public class DiscoveryEntity extends FilterHolderEntity {
public void setVersionFilterEntity(VersionFilterEntity versionFilterEntity) {
this.versionFilterEntity = versionFilterEntity;
}
public WeightFilterEntity getWeightFilterEntity() {
return weightFilterEntity;
}
public void setWeightFilterEntity(WeightFilterEntity weightFilterEntity) {
this.weightFilterEntity = weightFilterEntity;
}
}
\ No newline at end of file
package com.nepxion.discovery.common.entity;
/**
* <p>Title: Nepxion Discovery</p>
* <p>Description: Nepxion Discovery</p>
* <p>Copyright: Copyright (c) 2017-2050</p>
* <p>Company: Nepxion</p>
* @author Haojun Ren
* @version 1.0
*/
import java.io.Serializable;
import java.util.Map;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class WeightEntity implements Serializable {
private static final long serialVersionUID = 4242297554671632704L;
private String consumerServiceName;
private String providerServiceName;
private Map<String, Integer> weightMap;
public WeightEntity() {
}
public String getConsumerServiceName() {
return consumerServiceName;
}
public void setConsumerServiceName(String consumerServiceName) {
this.consumerServiceName = consumerServiceName;
}
public String getProviderServiceName() {
return providerServiceName;
}
public void setProviderServiceName(String providerServiceName) {
this.providerServiceName = providerServiceName;
}
public Map<String, Integer> getWeightMap() {
return weightMap;
}
public void setWeightMap(Map<String, Integer> weightMap) {
this.weightMap = weightMap;
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object object) {
return EqualsBuilder.reflectionEquals(this, object);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}
\ No newline at end of file
package com.nepxion.discovery.common.entity;
/**
* <p>Title: Nepxion Discovery</p>
* <p>Description: Nepxion Discovery</p>
* <p>Copyright: Copyright (c) 2017-2050</p>
* <p>Company: Nepxion</p>
* @author Haojun Ren
* @version 1.0
*/
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class WeightFilterEntity implements Serializable {
private static final long serialVersionUID = 7313443273653189837L;
private Map<String, List<WeightEntity>> weightEntityMap = new LinkedHashMap<String, List<WeightEntity>>();
public WeightFilterEntity() {
}
public Map<String, List<WeightEntity>> getWeightEntityMap() {
return weightEntityMap;
}
public void setWeightEntityMap(Map<String, List<WeightEntity>> weightEntityMap) {
this.weightEntityMap = weightEntityMap;
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object object) {
return EqualsBuilder.reflectionEquals(this, object);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}
\ No newline at end of file
......@@ -18,10 +18,13 @@ public class ConfigConstant {
public static final String WHITELIST_ELEMENT_NAME = "whitelist";
public static final String COUNT_ELEMENT_NAME = "count";
public static final String VERSION_ELEMENT_NAME = "version";
public static final String WEIGHT_ELEMENT_NAME = "weight";
public static final String FILTER_VALUE_ATTRIBUTE_NAME = "filter-value";
public static final String SERVICE_NAME_ATTRIBUTE_NAME = "service-name";
public static final String CONSUMER_SERVICE_NAME_ATTRIBUTE_NAME = "consumer-service-name";
public static final String PROVIDER_SERVICE_NAME_ATTRIBUTE_NAME = "provider-service-name";
public static final String CONSUMER_VERSION_VALUE_ATTRIBUTE_NAME = "consumer-version-value";
public static final String PROVIDER_VERSION_VALUE_ATTRIBUTE_NAME = "provider-version-value";
public static final String PROVIDER_WEIGHT_VALUE_ATTRIBUTE_NAME = "provider-weight-value";
public static final String SEPARATE = "=";
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ package com.nepxion.discovery.plugin.configcenter.parser.xml;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
......@@ -25,13 +26,15 @@ import org.slf4j.LoggerFactory;
import com.nepxion.discovery.common.constant.DiscoveryConstant;
import com.nepxion.discovery.common.entity.CountFilterEntity;
import com.nepxion.discovery.common.entity.DiscoveryEntity;
import com.nepxion.discovery.common.entity.VersionEntity;
import com.nepxion.discovery.common.entity.FilterHolderEntity;
import com.nepxion.discovery.common.entity.FilterType;
import com.nepxion.discovery.common.entity.HostFilterEntity;
import com.nepxion.discovery.common.entity.RegisterEntity;
import com.nepxion.discovery.common.entity.RuleEntity;
import com.nepxion.discovery.common.entity.VersionEntity;
import com.nepxion.discovery.common.entity.VersionFilterEntity;
import com.nepxion.discovery.common.entity.WeightEntity;
import com.nepxion.discovery.common.entity.WeightFilterEntity;
import com.nepxion.discovery.common.exception.DiscoveryException;
import com.nepxion.discovery.plugin.configcenter.constant.ConfigConstant;
import com.nepxion.discovery.plugin.configcenter.parser.xml.dom4j.Dom4JReader;
......@@ -129,6 +132,8 @@ public class XmlConfigParser implements PluginConfigParser {
parseHostFilter(childElement, ConfigConstant.WHITELIST_ELEMENT_NAME, discoveryEntity);
} else if (StringUtils.equals(childElement.getName(), ConfigConstant.VERSION_ELEMENT_NAME)) {
parseVersionFilter(childElement, discoveryEntity);
} else if (StringUtils.equals(childElement.getName(), ConfigConstant.WEIGHT_ELEMENT_NAME)) {
parseWeightFilter(childElement, discoveryEntity);
}
}
}
......@@ -295,6 +300,71 @@ public class XmlConfigParser implements PluginConfigParser {
discoveryEntity.setVersionFilterEntity(versionFilterEntity);
}
@SuppressWarnings("rawtypes")
private void parseWeightFilter(Element element, DiscoveryEntity discoveryEntity) {
WeightFilterEntity weightFilterEntity = discoveryEntity.getWeightFilterEntity();
if (weightFilterEntity != null) {
throw new DiscoveryException("Allow only one element[" + ConfigConstant.WEIGHT_ELEMENT_NAME + "] to be configed");
}
weightFilterEntity = new WeightFilterEntity();
Map<String, List<WeightEntity>> weightEntityMap = weightFilterEntity.getWeightEntityMap();
for (Iterator elementIterator = element.elementIterator(); elementIterator.hasNext();) {
Object childElementObject = elementIterator.next();
if (childElementObject instanceof Element) {
Element childElement = (Element) childElementObject;
if (StringUtils.equals(childElement.getName(), ConfigConstant.SERVICE_ELEMENT_NAME)) {
WeightEntity weightEntity = new WeightEntity();
Attribute consumerServiceNameAttribute = childElement.attribute(ConfigConstant.CONSUMER_SERVICE_NAME_ATTRIBUTE_NAME);
if (consumerServiceNameAttribute == null) {
throw new DiscoveryException("Attribute[" + ConfigConstant.CONSUMER_SERVICE_NAME_ATTRIBUTE_NAME + "] in element[" + childElement.getName() + "] is missing");
}
String consumerServiceName = consumerServiceNameAttribute.getData().toString().trim();
weightEntity.setConsumerServiceName(consumerServiceName);
Attribute providerServiceNameAttribute = childElement.attribute(ConfigConstant.PROVIDER_SERVICE_NAME_ATTRIBUTE_NAME);
if (providerServiceNameAttribute == null) {
throw new DiscoveryException("Attribute[" + ConfigConstant.PROVIDER_SERVICE_NAME_ATTRIBUTE_NAME + "] in element[" + childElement.getName() + "] is missing");
}
String providerServiceName = providerServiceNameAttribute.getData().toString().trim();
weightEntity.setProviderServiceName(providerServiceName);
Attribute providerWeightValueAttribute = childElement.attribute(ConfigConstant.PROVIDER_WEIGHT_VALUE_ATTRIBUTE_NAME);
if (providerWeightValueAttribute == null) {
throw new DiscoveryException("Attribute[" + ConfigConstant.PROVIDER_WEIGHT_VALUE_ATTRIBUTE_NAME + "] in element[" + childElement.getName() + "] is missing");
}
String providerWeightValue = providerWeightValueAttribute.getData().toString().trim();
Map<String, Integer> weightMap = new LinkedHashMap<String, Integer>();
List<String> providerWeightValueList = parseList(providerWeightValue);
for (String value : providerWeightValueList) {
String[] valueArray = StringUtils.split(value, ConfigConstant.SEPARATE);
String version = valueArray[0].trim();
int weight = Integer.valueOf(valueArray[1].trim());
if (weight < 0) {
throw new DiscoveryException("Attribute[" + ConfigConstant.PROVIDER_WEIGHT_VALUE_ATTRIBUTE_NAME + "] in element[" + childElement.getName() + "] has weight value less than 0");
}
weightMap.put(version, weight);
}
weightEntity.setWeightMap(weightMap);
List<WeightEntity> weightEntityList = weightEntityMap.get(consumerServiceName);
if (weightEntityList == null) {
weightEntityList = new ArrayList<WeightEntity>();
weightEntityMap.put(consumerServiceName, weightEntityList);
}
weightEntityList.add(weightEntity);
}
}
}
discoveryEntity.setWeightFilterEntity(weightFilterEntity);
}
private List<String> parseList(String value) {
if (StringUtils.isEmpty(value)) {
return null;
......
package com.nepxion.discovery.plugin.framework.decorator;
/**
* <p>Title: Nepxion Discovery</p>
* <p>Description: Nepxion Discovery</p>
* <p>Copyright: Copyright (c) 2017-2050</p>
* <p>Company: Nepxion</p>
* @author Haojun Ren
* @version 1.0
*/
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.nepxion.discovery.common.entity.DiscoveryEntity;
import com.nepxion.discovery.common.entity.RuleEntity;
import com.nepxion.discovery.common.entity.WeightEntity;
import com.nepxion.discovery.common.entity.WeightFilterEntity;
import com.nepxion.discovery.common.exception.DiscoveryException;
import com.nepxion.discovery.plugin.framework.adapter.PluginAdapter;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
public class ZoneAvoidanceRuleDecorator extends ZoneAvoidanceRule {
private static final Logger LOG = LoggerFactory.getLogger(ZoneAvoidanceRuleDecorator.class);
@Autowired
private PluginAdapter pluginAdapter;
private final Random random = new Random();
@Override
public Server choose(Object key) {
RuleEntity ruleEntity = pluginAdapter.getRule();
if (ruleEntity == null) {
return super.choose(key);
}
DiscoveryEntity discoveryEntity = ruleEntity.getDiscoveryEntity();
if (discoveryEntity == null) {
return super.choose(key);
}
WeightFilterEntity weightFilterEntity = discoveryEntity.getWeightFilterEntity();
if (weightFilterEntity == null) {
return super.choose(key);
}
Map<String, List<WeightEntity>> weightEntityMap = weightFilterEntity.getWeightEntityMap();
if (MapUtils.isEmpty(weightEntityMap)) {
return super.choose(key);
}
String serviceId = pluginAdapter.getServiceId();
List<WeightEntity> weightEntityList = weightEntityMap.get(serviceId);
if (CollectionUtils.isEmpty(weightEntityList)) {
return super.choose(key);
}
List<Server> eligibleServers = getPredicate().getEligibleServers(getLoadBalancer().getAllServers(), key);
try {
return choose(eligibleServers, weightEntityList);
} catch (Exception e) {
return super.choose(key);
}
}
private Server choose(List<Server> serverList, List<WeightEntity> weightEntityList) {
if (CollectionUtils.isEmpty(serverList)) {
return null;
}
int[] weights = new int[serverList.size()];
for (int i = 0; i < serverList.size(); i++) {
Server server = serverList.get(i);
int weight = getWeight(server, weightEntityList);
if (weight > 0) {
weights[i] = weight;
}
}
int index = getIndex(weights);
return serverList.get(index);
}
private int getIndex(int[] weights) {
// 次序号/权重区间值
int[][] weightHolder = new int[weights.length][2];
// 总权重
int totalWeight = 0;
// 赋值次序号和区间值累加的数组值,从小到大排列
// 例如,对于权重分别为20,40, 60的三个服务,将形成[0, 20),[20, 60),[60, 120]三个区间
for (int i = 0; i < weights.length; i++) {
if (weights[i] <= 0) {
continue;
}
totalWeight += weights[i];
weightHolder[i][0] = i;
weightHolder[i][1] = totalWeight;
}
// 获取介于0(含)和n(不含)伪随机,均匀分布的int值
int hitWeight = random.nextInt(totalWeight) + 1; // [1, totalWeight)
for (int i = 0; i < weightHolder.length; i++) {
if (hitWeight <= weightHolder[i][1]) {
return weightHolder[i][0];
}
}
return weightHolder[0][0];
}
private int getWeight(Server server, List<WeightEntity> weightEntityList) {
String providerServiceId = server.getMetaInfo().getAppName();
String providerVersion = pluginAdapter.getServerVersion(server);
for (WeightEntity weightEntity : weightEntityList) {
String providerServiceName = weightEntity.getProviderServiceName();
if (StringUtils.equalsIgnoreCase(providerServiceName, providerServiceId)) {
Map<String, Integer> weightMap = weightEntity.getWeightMap();
Integer weight = weightMap.get(providerVersion);
if (weight != null) {
return weight;
} else {
LOG.error("Weight isn't configed for serviceId={}, version={}", providerServiceId, providerVersion);
throw new DiscoveryException("Weight isn't configed for serviceId=" + providerServiceId + ", version=" + providerVersion);
}
}
}
return -1;
}
}
\ No newline at end of file
......@@ -11,12 +11,12 @@ package com.nepxion.discovery.plugin.strategy.discovery;
import org.springframework.util.Assert;
import com.nepxion.discovery.plugin.framework.decorator.ZoneAvoidanceRuleDecorator;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.AvailabilityPredicate;
import com.netflix.loadbalancer.CompositePredicate;
import com.netflix.loadbalancer.PredicateBasedRule;
public class DiscoveryEnabledRule extends PredicateBasedRule {
public class DiscoveryEnabledRule extends ZoneAvoidanceRuleDecorator {
private final CompositePredicate predicate;
private final DiscoveryEnabledPredicate discoveryEnabledPredicate;
......
......@@ -62,5 +62,17 @@
<!-- 表示消费端服务b的1.1,允许访问提供端服务c的1.2版本 -->
<service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" consumer-version-value="1.1" provider-version-value="1.2"/>
</version>
<!-- 服务发现的多权重灰度访问控制 -->
<!-- service-name,表示服务名 -->
<!-- version-value,表示版本对应的权重值,格式为"版本值=权重值",如果多个用“;”分隔,不允许出现空格 -->
<!-- 权重策略介绍 -->
<!-- 1. 标准配置,举例如下 -->
<!-- <service consumer-service-name="a" provider-service-name="b" provider-weight-value="1.0=70;1.1=30"/> 表示消费端访问提供端的时候,提供端的1.0版本提供70%的权重流量,1.1版本提供30%的权重流量 -->
<!-- 2. 尽量为线上所有版本都赋予权重值 -->
<weight>
<!-- 表示消费端服务b访问提供端服务c的时候,提供端服务c的1.0版本提供70%的权重流量,1.1版本提供30%的权重流量 -->
<service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" provider-weight-value="1.0=70;1.1=30"/>
</weight>
</discovery>
</rule>
\ No newline at end of file
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