支持预发布、灰度发布

This commit is contained in:
tanghc
2019-08-02 20:16:14 +08:00
parent 6125625b87
commit 202267b686
40 changed files with 947 additions and 123 deletions

View File

@@ -0,0 +1,42 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名config_gray_userkey
* 备注灰度发布用户key
*
* @author tanghc
*/
@Table(name = "config_gray_userkey")
@Data
public class ConfigGrayUserkey {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段id */
private Long id;
/** instanceId, 数据库字段instance_id */
private String instanceId;
/** 用户key多个用引文逗号隔开, 数据库字段user_key_content */
private String userKeyContent;
/** 需要灰度的接口goods.get=1.2,order.list=1.2, 数据库字段name_version_content */
private String nameVersionContent;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -0,0 +1,116 @@
package com.gitee.sop.gateway.loadbalancer;
import com.gitee.sop.gateway.manager.UserKeyManager;
import com.gitee.sop.gatewaycommon.bean.SpringContext;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.param.Param;
import com.gitee.sop.gatewaycommon.param.ParamNames;
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* 预发布、灰度环境选择参考自https://segmentfault.com/a/1190000017412946
*
* @author tanghc
*/
public class EnvironmentServerChooser extends BaseServerChooser {
private static final String MEDATA_KEY_ENV = "env";
private static final String ENV_PRE_VALUE = "pre";
private static final String ENV_GRAY_VALUE = "gray";
/**
* 预发布机器域名
*/
private static final String PRE_DOMAIN = "localhost";
@Override
protected boolean match(Server server) {
// eureka存储的metadata
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String env = metadata.get(MEDATA_KEY_ENV);
return StringUtils.isNotBlank(env);
}
/**
* 这里判断客户端能否访问可以根据ip地址域名header内容来决定是否可以访问预发布环境
*
* @param server 服务器实例
* @param request request
* @return
*/
@Override
protected boolean canVisit(Server server, HttpServletRequest request) {
// eureka存储的metadata
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String env = metadata.get(MEDATA_KEY_ENV);
boolean canVisit;
switch (env) {
case ENV_PRE_VALUE:
canVisit = canVisitPre(server, request);
break;
case ENV_GRAY_VALUE:
canVisit = canVisitGray(server, request);
break;
default:
canVisit = false;
}
return canVisit;
}
/**
* 通过判断hostname来确定是否是预发布请求可修改此方法实现自己想要的
*
* @param request request
* @return 返回true可以进入到预发环境
*/
protected boolean canVisitPre(Server server, HttpServletRequest request) {
String serverName = request.getServerName();
return PRE_DOMAIN.equals(serverName);
}
/**
* 能否进入灰度环境,可修改此方法实现自己想要的
*
* @param request request
* @return 返回true可以进入到预发环境
*/
protected boolean canVisitGray(Server server, HttpServletRequest request) {
ApiParam apiParam = ZuulContext.getApiParam();
UserKeyManager userKeyManager = SpringContext.getBean(UserKeyManager.class);
boolean canVisit = false;
if (this.isGrayUser(apiParam, userKeyManager, server, request)) {
// 指定灰度版本号
String instanceId = server.getId();
String newVersion = userKeyManager.getVersion(instanceId, apiParam.fetchNameVersion());
if (newVersion != null) {
RequestContext.getCurrentContext().getZuulRequestHeaders().put(ParamNames.HEADER_VERSION_NAME, newVersion);
canVisit = true;
}
}
return canVisit;
}
/**
* 是否是灰度用户
*
* @param param 接口参数
* @param userKeyManager userKey管理
* @param server 服务器实例
* @param request request
* @return true
*/
protected boolean isGrayUser(Param param, UserKeyManager userKeyManager, Server server, HttpServletRequest request) {
String instanceId = server.getId();
// 这里的灰度用户为appKey包含此appKey则为灰度用户允许访问
String appKey = param.fetchAppKey();
return userKeyManager.containsKey(instanceId, appKey);
}
}

View File

@@ -1,55 +0,0 @@
package com.gitee.sop.gateway.loadbalancer;
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
/**
* 预发布环境选择参考自https://segmentfault.com/a/1190000017412946
*
* @author tanghc
*/
public class PreEnvironmentServerChooser extends BaseServerChooser {
private static final String MEDATA_KEY_ENV = "env";
private static final String ENV_PRE_VALUE = "pre";
private static final String HOST_NAME = "localhost";
@Override
protected boolean match(Server server) {
// eureka存储的metadata
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String env = metadata.get(MEDATA_KEY_ENV);
return StringUtils.isNotBlank(env);
}
/**
* 这里判断客户端能否访问可以根据ip地址域名header内容来决定是否可以访问预发布环境
* @param server 服务器实例
* @param request request
* @return
*/
@Override
protected boolean canVisit(Server server, HttpServletRequest request) {
// eureka存储的metadata
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String env = metadata.get(MEDATA_KEY_ENV);
return Objects.equals(ENV_PRE_VALUE, env) && canClientVisit(request);
}
/**
* 通过判断hostname来确定是否是预发布请求如果需要通过其它条件判断修改此方法
* @param request request
* @return 返回true可以进入到预发环境
*/
protected boolean canClientVisit(HttpServletRequest request) {
String serverName = request.getServerName();
return HOST_NAME.equals(serverName);
}
}

View File

@@ -0,0 +1,33 @@
package com.gitee.sop.gateway.loadbalancer;
import lombok.Data;
import java.util.Map;
import java.util.Set;
/**
* @author tanghc
*/
@Data
public class ServiceGrayConfig {
/**
* 用户id
*/
private Set<String> userKeys;
/** 存放接口隐射关系key:nameversionvalue:newVersion */
private Map<String, String> grayNameVersion;
public boolean containsKey(Object userKey) {
return userKeys.contains(String.valueOf(userKey));
}
public String getVersion(String name) {
return grayNameVersion.get(name);
}
public void clear() {
this.userKeys.clear();
this.grayNameVersion.clear();
}
}

View File

@@ -17,13 +17,13 @@ public class SopPropertiesFactory extends PropertiesFactory {
*/
private static final String PROPERTIES_KEY = "zuul.custom-rule-classname";
private static final String CUSTOM_RULE_CLASSNAME = PreEnvironmentServerChooser.class.getName();
private static final String CUSTOM_RULE_CLASSNAME = EnvironmentServerChooser.class.getName();
@Autowired
private Environment environment;
/**
* 配置文件配置:<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
* 配置文件配置:<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.EnvironmentServerChooser
* @param clazz
* @param name serviceId
* @return 返回class全限定名

View File

@@ -0,0 +1,132 @@
package com.gitee.sop.gateway.manager;
import com.alibaba.fastjson.JSON;
import com.gitee.sop.gateway.entity.ConfigGrayUserkey;
import com.gitee.sop.gateway.loadbalancer.ServiceGrayConfig;
import com.gitee.sop.gateway.mapper.ConfigGrayUserkeyMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.UserKeyDefinition;
import com.gitee.sop.gatewaycommon.manager.ZookeeperContext;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 存放用户key这里放在本机内容如果灰度发布保存的用户id数量偏多可放在redis中
*
* @author tanghc
*/
@Service
@Slf4j
public class UserKeyManager {
/**
* KEY:instanceId
*/
private Map<String, ServiceGrayConfig> serviceUserKeyMap = Maps.newConcurrentMap();
@Autowired
private Environment environment;
@Autowired
private ConfigGrayUserkeyMapper configGrayUserkeyMapper;
public boolean containsKey(String serviceId, Object userKey) {
if (serviceId == null || userKey == null) {
return false;
}
return this.getServiceGrayConfig(serviceId).containsKey(userKey);
}
public String getVersion(String serviceId, String nameVersion) {
if (serviceId == null || nameVersion == null) {
return null;
}
return this.getServiceGrayConfig(serviceId).getVersion(nameVersion);
}
/**
* 设置用户key
*
* @param configGrayUserkey 灰度配置
*/
public void setServiceGrayConfig(String serviceId, ConfigGrayUserkey configGrayUserkey) {
if (configGrayUserkey == null) {
return;
}
this.clear(serviceId);
String userKeyData = configGrayUserkey.getUserKeyContent();
String nameVersionContent = configGrayUserkey.getNameVersionContent();
String[] userKeys = StringUtils.split(userKeyData, ',');
String[] nameVersionList = StringUtils.split(nameVersionContent, ',');
log.info("添加userKeyuserKeys.length:{}, nameVersionList:{}", userKeys.length, Arrays.toString(nameVersionList));
List<String> list = Stream.of(userKeys).collect(Collectors.toList());
ServiceGrayConfig serviceGrayConfig = getServiceGrayConfig(serviceId);
serviceGrayConfig.getUserKeys().addAll(list);
Map<String, String> grayNameVersion = serviceGrayConfig.getGrayNameVersion();
for (String nameVersion : nameVersionList) {
String[] nameVersionInfo = StringUtils.split(nameVersion, '=');
String name = nameVersionInfo[0];
String version = nameVersionInfo[1];
grayNameVersion.put(name, version);
}
}
/**
* 清空用户key
*/
public void clear(String serviceId) {
getServiceGrayConfig(serviceId).clear();
}
public ServiceGrayConfig getServiceGrayConfig(String serviceId) {
ServiceGrayConfig serviceGrayConfig = serviceUserKeyMap.get(serviceId);
if (serviceGrayConfig == null) {
serviceGrayConfig = new ServiceGrayConfig();
serviceGrayConfig.setUserKeys(Sets.newConcurrentHashSet());
serviceGrayConfig.setGrayNameVersion(Maps.newConcurrentMap());
serviceUserKeyMap.put(serviceId, serviceGrayConfig);
}
return serviceGrayConfig;
}
@PostConstruct
protected void after() throws Exception {
ZookeeperContext.setEnvironment(environment);
String isvChannelPath = ZookeeperContext.getUserKeyChannelPath();
ZookeeperContext.listenPath(isvChannelPath, nodeCache -> {
String nodeData = new String(nodeCache.getCurrentData().getData());
ChannelMsg channelMsg = JSON.parseObject(nodeData, ChannelMsg.class);
String data = channelMsg.getData();
UserKeyDefinition userKeyDefinition = JSON.parseObject(data, UserKeyDefinition.class);
String serviceId = userKeyDefinition.getServiceId();
switch (channelMsg.getOperation()) {
case "set":
ConfigGrayUserkey configGrayUserkey = configGrayUserkeyMapper.getByColumn("service_id", serviceId);
this.setServiceGrayConfig(serviceId, configGrayUserkey);
break;
case "clear":
clear(serviceId);
break;
default:
log.error("userKey消息错误的消息指令nodeData{}", nodeData);
}
});
}
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigGrayUserkey;
/**
* @author tanghc
*/
public interface ConfigGrayUserkeyMapper extends CrudMapper<ConfigGrayUserkey, Long> {
}