参考1:https://gitee.com/wangxinqiao/springcloud-gray
参考2:https://zhuanlan.zhihu.com/p/152217968
流程
1. 外部请求进入网关
2. 灰度拦截器根据灰度规则向线程变量中添加版本号(prod/test)
3. Feign拦截器从线程变量中取出版本号,并存入Feign请求头中,目的是为了让下游服务拿到版本号
4. Ribbon根据自定义负载均衡策略调用对应的服务
5. 下游服务的灰度拦截器从请求头中取出版本号,存入线程变量
6. 下游服务如果需要调用更下游的灰度服务,Feign拦截器从线程变量中取出版本号,并存入Feign请求头中;如果下游服务不再调用更下游的灰度服务,就不再需要Feign拦截器
7. 返回第4步
模拟场景
- service-common(公共服务)、service-zuul(网关服务)、service-A(灰度服务1)、service-B(灰度服务2)
- 外部调用:请求 --> service-zuul --> service-A
- 内部调用:请求 --> service-zuul --> service-A --> service-B(A --> B通过Fegin调用,Resttemplate方式同理)
- 灰度规则:如果请求头的userId为0,自动分发请求、userId为1请求v1服务,userId为2请求v2服务
- 灰度规则根据实际情况制定,这里做测试用
service-common
注意:以下代码均不能添加@Configuration注解
GrayMetadataRule
package com.cloud.common.gray;
import com.google.common.base.Optional;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 自定义负载均衡策略
*/
public class GrayMetadataRule extends ZoneAvoidanceRule {
public static final String META_DATA_KEY_VERSION = "version";
@Override
public Server choose(Object key) {
// 获取服务列表
List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
if (CollectionUtils.isEmpty(serverList)) {
return null;
}
// 线程变量中的版本号
String hystrixVer = CoreHeaderInterceptor.version.get();
//不是灰度服务,或者不做灰度分发的服务列表
List<Server> noMetaServerList = new ArrayList<>();
for (Server server : serverList) {
// 获取metadata
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
// metadata中的版本号
String metaVersion = metadata.get(META_DATA_KEY_VERSION);
if (!StringUtils.isEmpty(metaVersion)) {
if (metaVersion.equals(hystrixVer)) {
return server;
}
} else {
//如果metadata中没有版本号,说明此服务不是灰度服务
noMetaServerList.add(server);
}
}
if (!noMetaServerList.isEmpty()) {
return originChoose(noMetaServerList, key);
}
return null;
}
private Server originChoose(List<Server> noMetaServerList, Object key) {
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
MySelRule
package com.cloud.common.gray;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
/**
* 自定义负载均衡策略配置类
* Created by ${lyt} on 2021/3/9 14:56
*/
public class MySelRule {
@Bean
public IRule MyRule() {
return new GrayMetadataRule();
}
}
CoreHeaderInterceptor
package com.cloud.common.gray;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义HandlerInterceptorAdapter拦截器,
* 在到达服务之前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中
*/
public class CoreHeaderInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(CoreHeaderInterceptor.class);
public static final String HEADER_VERSION = "version";
public static final HystrixRequestVariableDefault<String> version = new HystrixRequestVariableDefault<>();
public static void initHystrixRequestContext(String headerVer) {
logger.debug("headerVer:{}", headerVer);
if (!HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.initializeContext();
}
if (!StringUtils.isEmpty(headerVer)) {
CoreHeaderInterceptor.version.set(headerVer);
} else {
CoreHeaderInterceptor.version.set("");
}
}
public static void shutdownHystrixRequestContext() {
if (HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.getContextForCurrentThread().shutdown();
}
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
CoreHeaderInterceptor.initHystrixRequestContext(request.getHeader(CoreHeaderInterceptor.HEADER_VERSION));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
CoreHeaderInterceptor.shutdownHystrixRequestContext();
}
}
service-zuul
启动类加入@RibbonClients(defaultConfiguration = MySelRule.class) //使用RibbonClients,避免配置成全局ribbonServerList
GrayFilter
package com.cloud.gateway.filter;
import com.cloud.common.gray.CoreHeaderInterceptor;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 灰度服务过滤器
*/
@Component
public class GrayFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
//userId
String userId = request.getHeader("userId");
if ("0".equals(userId)) {
// 将版本信息存入到线程变量中
CoreHeaderInterceptor.initHystrixRequestContext("");
// 将版本信息存入请求头中
context.addZuulRequestHeader(CoreHeaderInterceptor.HEADER_VERSION, "");
}else if ("1".equals(userId)) {
// 将版本信息存入到线程变量中
CoreHeaderInterceptor.initHystrixRequestContext("v1");
// 将版本信息存入请求头中
context.addZuulRequestHeader(CoreHeaderInterceptor.HEADER_VERSION, "v1");
}else if ("2".equals(userId)) {
// 将版本信息存入到线程变量中
CoreHeaderInterceptor.initHystrixRequestContext("v2");
// 将版本信息存入请求头中
context.addZuulRequestHeader(CoreHeaderInterceptor.HEADER_VERSION, "v2");
}
return null;
}
}
CoreFeignRequestInterceptor
package com.cloud.gateway.filter;
import com.cloud.common.gray.CoreHeaderInterceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
/**
* feign拦截器,目的是让下游服务拿到版本号
* 如果下游服务不再使用Feign调用其它服务,比如service-B不再调用其它灰度服务的话,service-A就不需要这个类
*/
@Configuration
public class CoreFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//从线程变量中拿到版本号
String hystrixVer = CoreHeaderInterceptor.version.get();
//向请求头加入版本号
template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
}
}
service-A 和 service-B都启动两个,并在eureka中加入版本号,版本号分别为v1和v2
service-A
启动类加入@RibbonClients(defaultConfiguration = MySelRule.class) //使用RibbonClients,避免配置成全局ribbonServerList
bootstrap.yml
eureka:
instance:
metadata-map:
version: v1/v2
GrayFilter
package com.cloud.service-A.filter;
import com.cloud.common.gray.CoreHeaderInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 灰度发布过滤器(将版本信息存入到线程变量中)
* Created by ${lyt} on 2021/3/10 9:18
*/
@Slf4j
@Component
public class GrayFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
// 从请求头中取出版本号
String version = httpServletRequest.getHeader(CoreHeaderInterceptor.HEADER_VERSION);
// 将版本信息存入到线程变量中
CoreHeaderInterceptor.initHystrixRequestContext(version);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
service-B
bootstrap.yml
eureka:
instance:
metadata-map:
version: v1/v2
- 因为service-B不再调用其它灰度服务,所以ribbon不用自定义的负载均衡策略,也就不用在启动类中添加@RibbonClients注解,也不用配置灰度拦截器和Feign拦截器
- 如果service-B的下游服务需要调用其它灰度服务,就需要在service-B中配置灰度拦截器和Feign拦截器,下游服务的启动类需要添加@RibbonClients注解