mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-11 21:57:56 +08:00
支持预发布、灰度发布
This commit is contained in:
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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:nameversion,value: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();
|
||||
}
|
||||
}
|
@@ -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全限定名
|
||||
|
@@ -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("添加userKey,userKeys.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);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -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> {
|
||||
}
|
Reference in New Issue
Block a user