一、前提
1、所有系统前后端都要至少部署双节点
2、灰度的前端域名最好和生产的域名不一致
3、所有项目是springcloud+nacos+rocketmq架构
二、底层jar包升级
1、类文件GrayReqInterceptor:
package com.ms.roster.gray.aspect;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.ms.common.utils.StringUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
*
* @ClassName: GrayReqInterceptor
* @Description: 灰度拦截器
* @author dingjy
* @date 2023年9月6日 下午4:28:11
*/
@Component
public class GrayReqInterceptor implements RequestInterceptor {
private Logger logger = LoggerFactory.getLogger(GrayReqInterceptor.class);
private static final String VERSION_KEY = "versionId";
/**
*
* Title: apply Description: 处理请求头参数携带问题
*
* @param requestTemplate
* @see feign.RequestInterceptor#apply(feign.RequestTemplate)
*/
@Override
public void apply(RequestTemplate requestTemplate) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String versionId = request.getHeader(VERSION_KEY);
logger.info("apply_versionId:{}",versionId);
if (StringUtils.isNotBlank(versionId)) {
requestTemplate.header(VERSION_KEY, versionId);
}
}
}
2、类文件GrayRule:
package com.ms.roster.gray.aspect;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.fastjson.JSONArray;
import com.google.common.base.Optional;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
/**
*
* @ClassName: GrayRule
* @Description: 灰度规则
* @author dingjy
* @date 2023年9月6日 下午4:29:11
*/
@Configuration
@RibbonClients(defaultConfiguration = GrayRule.class)
public class GrayRule extends ZoneAvoidanceRule {
private Logger log = LoggerFactory.getLogger(GrayRule.class);
private static final String VERSION_KEY = "versionId";
@Override
public Server choose(Object key) {
// 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
// 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询 getPredicate().chooseRoundRobinAfterFiltering()
List<Server> serverList = this.getServers();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(serverList, key);
return server.isPresent() ? server.get() : null;
}
/**
* 灰度路由过滤服务实例
*
* 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
* 则不走灰度路由,按原有轮询机制轮询所有
* @throws Exception
*/
protected List<Server> getServers() {
// 获取spring cloud默认负载均衡器
// 获取所有待选的服务
List<Server> allServers = getLoadBalancer().getReachableServers();
log.info("choose_allServers:{}", JSONArray.toJSONString(allServers));
if (CollectionUtils.isEmpty(allServers)) {
log.error("没有可用的服务实例");
}
//获取版本号
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String versionId = request.getHeader(VERSION_KEY);
// 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
if (StringUtils.isBlank(versionId)) {
//这里需要过滤掉灰度服务实例
List<Server> list = allServers.stream().filter(f -> {
// 获取服务实例在注册中心上的元数据
Map<String, String> metadata = ((NacosServer) f).getMetadata();
// 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
if (null != metadata && StringUtils.isNotBlank(metadata.get(VERSION_KEY))) {
return false;
}
return true;
}).collect(Collectors.toList());
return list;
}
// 开始灰度规则匹配过滤
List<Server> filterServer = new ArrayList<>();
for (Server server : allServers) {
// 获取服务实例在注册中心上的元数据
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
if (null != metadata && StringUtils.equals(versionId, metadata.get(VERSION_KEY))) {
filterServer.add(server);
}
}
// 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
if (CollectionUtils.isEmpty(filterServer)) {
log.error("灰度路由规则失效,没有找到期望的版本实例,versionId={},走原先的非灰度版本", versionId);
return allServers;
}
log.info("choose_filterServer:{}", JSONArray.toJSONString(filterServer));
return filterServer;
}
}
3、类文件RequestAttributeHystrixConcurrencyStrategy:
package com.ms.roster.gray.aspect;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
/**
*
* @ClassName: RequestHeaderHystrixConcurrencyStrategy
* @Description: 断路器策略
* @author dingjy
* @date 2023年9月6日 下午5:56:31
*/
@Component
public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private Logger log = LoggerFactory.getLogger(RequestAttributeHystrixConcurrencyStrategy.class);
private HystrixConcurrencyStrategy delegate;
public RequestAttributeHystrixConcurrencyStrategy() {
try {
log.info("加载RequestAttributeHystrixConcurrencyStrategy");
this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.delegate instanceof RequestAttributeHystrixConcurrencyStrategy) {
// Welcome to singleton hell...
return;
}
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy();
this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);
HystrixPlugins.reset();
HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
}
}
private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) {
if (log.isDebugEnabled()) {
log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy [" + this.delegate + "],"
+ "eventNotifier [" + eventNotifier + "]," + "metricPublisher [" + metricsPublisher + "],"
+ "propertiesStrategy [" + propertiesStrategy + "]," + "]");
log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
}
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return new WrappedCallable<>(callable, requestAttributes);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return this.delegate.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
return this.delegate.getRequestVariable(rv);
}
static class WrappedCallable<T> implements Callable<T> {
private final Callable<T> target;
private final RequestAttributes requestAttributes;
public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
this.target = target;
this.requestAttributes = requestAttributes;
}
@Override
public T call() throws Exception {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
return target.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}
4、将GrayReqInterceptor、GrayRule、RequestAttributeHystrixConcurrencyStrategy 集成进ops-cloud底层项目中,并将要引用的项目升级至ops-cloud的相关的版本,比如:
<parent>
<groupId>ms.platform</groupId>
<artifactId>ms-dependency</artifactId>
<version>1.1.9-SNAPSHOT</version>
</parent>
三、配置中心新增灰度域名
找到对应dataId下的配置文件,编辑Beta发布,填写需要灰度的服务器,并添加灰度标识:
spring.cloud.nacos.discovery.metadata.versionId=2
注意:versionId=2为灰度标识,后面所有灰度服务的灰度topic创建和消费者,都是要基于此灰度标识,示例如下,一定要改写生产和消费代码,读取nacos配置信息,topic、tag、消费者组上都需要加上_2来表示灰度环境:
灰度topic:ms-app-econtract-2022_2
灰度消费者组:ms-app-roster-2020_consumer_2
四、前端添加灰度标识:
1、在nginx入口处,给header添加versionId属性: