SpringCloud的调用链
SpringCloud微服务中之间的调用通常使用Feign方式和RestTemplate方式(较少使用)。
如果需要指定服务之间的调用, 首先要给各个服务添加唯一标识, 使用一些特殊的标记, 如版本号flag或者version等;
其次,要干预微服务中Ribbon的默认轮询调用机制, 我们需要根据微服务的版本等不同, 来进行调用。
最后, 在服务之间, 需要传递调用链路的信息, 我们可以在请求头中,添加调用链路的信息。
整理思路为:
- 1.在请求头中添加调用链路信息(有网关的话,这一步可以在网关做)
- 2.微服务之间调用时,使用feign拦截器,增强请求头
- 3.微服务调用选择时,根据指定的策略(如唯一标识版本等)从注册中心中获取指定的服务,并筛选目标进行调用
关键实现
请求头:x-gray-info
整个链路的上下文信息如何保存?header信息的读取
这涉及跨线程之间的数据传递。
header信息如何读取?
Feign在调用时并不是用的原先的Request,而是内部新建了一个Request,其中复制了请求的URL、请求参数一些信息,但是请求头并没有复制过去,因此openFeign调用会丢失请求头中的信息。
但是可以通过实现RequestInterceptor将原先的请求头给复制过去。
Feign拦截器
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
/**
* feign接口拦截, 添加上灰度路由请求头
* @param template
*/
@Override
public void apply(RequestTemplate template) {
String header = null;
try {
header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("x-gray-info");
if (null == header || header.isEmpty()) {
return;
}
} catch (Exception e) {
log.info("请求头信息获取失败, error: {}", e.getMessage());
}
template.header("x-gray-info", header);
}
}
灰度路由规则类(继承ZoneAvoidanceRule类)
微服务在拦截处理后, Ribbon组件会从服务实例列表中获取一个实现进行转发, 且Ribbon默认的规则是ZoneAvoidanceRule类, 我们定义自己的规则, 只需要继承该类,重写choose方法即可。
GrayRouteRule 这个类是实现的关键
@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {
@Autowired
protected GrayRouteProp grayRouteProperties;
/**
* 参考 {@link PredicateBasedRule#choose(Object)}
*
*/
@Override
public Server choose(Object key) {
// 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
// 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询 getPredicate().chooseRoundRobinAfterFiltering()
Optional<Server> server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
return server.isPresent() ? server.get() : null;
}
/**
* 灰度路由过滤服务实例
*
* 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
* 则不走灰度路由,按原有轮询机制轮询所有
*/
protected List<Server> getServers() {
// 获取spring cloud默认负载均衡器
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) getLoadBalancer();
// 获取本次请求生效的灰度路由规则
RouteProp routeRule = this.getGrayRoute();
// 获取本次请求期望的服务版本号
String version = getDesiredVersion(routeRule, lb.getName());
// 获取所有待选的服务
List<Server> allServers = lb.getAllServers();
if (CollectionUtils.isEmpty(allServers)) {
return new ArrayList<>();
}
// 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
if (StringUtils.isEmpty(version)) {
return allServers;
}
// 开始灰度规则匹配过滤
List<Server> filterServer = new ArrayList<>();
for (Server server : allServers) {
// 获取服务实例在注册中心上的元数据
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
if (null != metadata && version.equals(metadata.get(GrayRouteProp.VERSION_KEY))) {
filterServer.add(server);
}
}
// 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
if (CollectionUtils.isEmpty(filterServer)) {
log.warn(String.format("没有找到版本version[%s]的服务[%s],灰度路由规则降级为原有的轮询机制!", version,
lb.getName()));
filterServer = allServers;
}
return filterServer;
}
/**
* 获取本次请求 期望的服务版本号
*
* @param routeRule 生效的配置规则
* @param appName 服务名
*/
protected String getDesiredVersion(RouteProp routeRule, String appName) {
// 取路由规则里指定要访问的微服务的版本号
String version = null;
if (routeRule != null) {
if (routeRule.getCustom() != null) {
// 优先取custom里指定版本
version = routeRule.getCustom().get(appName);
} else {
// custom里没有指定就找all里面设置的统一版本
version = routeRule.getAll();
}
}
return version;
}
/**
* 获取设置的灰度路由规则
*/
protected RouteProp getGrayRoute() {
// 确定路由规则(请求头优先,yml配置其次)
RouteProp routeRule;
String route_header = null;
try {
route_header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader(GrayRouteProp.GRAY_ROUTE);
} catch (Exception e) {
log.error("灰度路由从上下文获取路由请求头异常!");
}
if (!StringUtils.isEmpty(route_header)) {//header
routeRule = JSONObject.parseObject(route_header, RouteProp.class);
} else {
// yml配置
routeRule = grayRouteProperties.getRoute();
}
return routeRule;
}
}
以上的实现,可以实现灰度发布了。
但是有可能遇到的问题,Gateway => Loader Balaner => Service Cluster,在这个过程中,有可能产生数据无法透传的情况,下面是一些备选知识和选项,作为记录:
Feign透传灰度标记/数据是关键点
Feign透传灰度标记/数据。
在执行spring的过滤器chain之前会把当前的ServletRequestAttributes保存在当前线程的ThreadLocal里,所以后面要想获取ServletRequestAttributes必须在同一个线程里通过RequestContextHolder.getRequestAttributes()才能获取到。
使用Feign进行接口调用的时候,会使用到Hystrix进行资源隔离,隔离方式包含线程池隔离和信号量Seamphore隔离,而默认是使用线程池隔离的,所以在使用Feign进行接口调用时,会在新的线程里进行Feign接口调用操作,故而无法通过RequestContextHolder.getRequestAttributes()获取前一个线程里设置的attribute,所以原来的实现方式不可行,从上面的解释中可以得知,只要是在同一个线程里就可以获取到线程里设置的attribute,恰好信号量Seamphore隔离就是在同一个线程里,于是就有了第一个解决方案。
通过信号量Seamphore隔离实现user_id的header传递
只需要在配置文件里增加信号量Seamphore隔离的配置,就可以获取到线程里配置的hystrix attribute
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
ThreadLocal ? 还是其他?
ThreadLocal它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程。
即使JDK扩展出了一个子类:InheritableThreadLocal,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。倘若两个线程间真的八竿子打不着,比如分别位于两个线程池内的线程,它们之间要传递数据该如何传递数据?
在实际生产中,线程一般不可能孤立的独立去运行,而是交给线程池去调度处理。所以实际上几乎没有纯正的父子线程的关系存在,而若有这种需求大多是线程池与线程池之间的线程联系。
通用解决方案
- 1.阿里的TransmittableThreadLocal 工具包
- 2.可以引入Sleuth等工具包
阿里的TransmittableThreadLocal使用场景,官方流出了其四大使用场景:
- 分布式跟踪系统(链路追踪)
- 日志收集记录系统上下文(MDC)
- Session级Cache
- 应用容器或上层框架跨应用代码给下层SDK传递信息