SpringCloud实战(十七)-基于Ribbon动态路由实现:调用链控制/版本控制/灰度发布(粒度更细,可以控制到每一个微服务模块版本的路由)
Posted 张志翔ۤ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud实战(十七)-基于Ribbon动态路由实现:调用链控制/版本控制/灰度发布(粒度更细,可以控制到每一个微服务模块版本的路由)相关的知识,希望对你有一定的参考价值。
Demo代码地址:https://download.csdn.net/download/qq_19734597/21454192
有什么用
- 可实现一套调用链管理工具,将管理好的调用链保存于redis中,实现一个ServiceLancherHandler类,从redis中取指定服务的访问tag,实现调用链管理
- 对服务添加自定义tag,例如blue/green,比如有一个服务A,线上运行版本为blue,如果需要升级服务,可先发布版本为green的服务A,然后在测试机中添加指定A:blue的header,可进行线上测试
如果测试没问题,可通过调用链管理工具将redis中A:blue改为A:green,实现流量不停机切换,解决服务刷新延迟问题,如果切换后发现green版本有bug,可随时切换为blue. - local环境无需搭建所有依赖服务,例如正在开发服务B的一些新本功能,可在启动B时打上自己的tag,并注册上dev环境的eureka,测试时可在header中指定B:myName,将自己发出的请求在访问B服务时转发到本机启动的B服务.
- 解决eureka缓存以及ribbon缓存服务注册表导致本地缓存刷新延迟从而在服务切换过程中多次请求已下线服务问题
- 彻底解决server.enable-self-preservation eureka保护模式导致应用无法下线问题
- 其他玩法请大胆想象...
注:下图中多次出现要判断Header中是否存在A:Gree,B:Gree(这里的Gree和Blue,我理解的应该是具体的某个微服务模块版本号、姓名或者其他规则,例如:A:v1.1.0,B:v1.3.0,C:zhangzhixiang),但是没有说明是什么时候把A:Gree,B:Gree放到Header的,我认为应该是在网关那块放入的,可以在配置中心配置一个灰度测试用户列表,访问网关时,在这个列表中的用户会在Header中存入灰度规则(这个灰度规则也是配置好的,可以放到配置中心、Redis、Mysql中,这个用户所对应的灰度路由规则,可以设置任意微服务模块的路由规则),因为我们重写了Ribbon的路由器,所以每个微服务模块可以按照我们重写的规则来进行路由。在这个大前提下,就可以把下面图中的规则看懂了(ACTIVE列表对应线上正在运行的稳定版本的一整套微服务,默认请求是走ACTIVE中的微服务,灰度测试没有问题了,可以把ACTIVE列表进行升级,灰度测试有BUG,就先不升ACTIVE列表,继续在网关中通过配置灰度列表的方式切部分流量来做测试)。
灰度发布图示:
实现原理及实战
实现原理:ribbon支持自定义rule路由规则,且提供了基于eureka-instance-meta进行路由的实现,可以根据客户端注册在eureka是的tag实现动态路由
核心依赖:
// 该依赖将指定MetadataAwareRule路由策略
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
核心类:RibbonFilterContextHolder, MetadataAwarePredicate.class
public class MetadataAwarePredicate extends DiscoveryEnabledPredicate {
public MetadataAwarePredicate() {
}
protected boolean apply(DiscoveryEnabledServer server) {
RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
Set<Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
Map<String, String> metadata = server.getInstanceInfo().getMetadata();
return metadata.entrySet().containsAll(attributes);
}
}
// 在zuul转发或者feign调用前写入动态路由策略,指定转发到有该tag-value的服务
RibbonFilterContextHolder.getCurrentContext()
.add("${tagName}", ${tagValue});
// 客户端服务配置
eureka:
instance:
metadata-map:
tagName1: tagValue1
tagName2: tagValue2
public abstract class ServiceLancherHandler {
protected boolean handle(String serviceId) {
String serviceLancher = getServiceLancher(serviceId);
if (StringUtils.isEmpty(serviceLancher)){
return false;
}
RibbonFilterContextHolder.clearCurrentContext();
RibbonFilterContextHolder.getCurrentContext()
.add("lancher", serviceLancher);
return true;
}
/**
子类通过实现该方法可自定义访问策略
*/
abstract String getServiceLancher(String serviceId);
}
1、zuul网关转发到指定tag的服务关键代码(api-gateway)
// 配置指定tag的职责链,
// 例如可定义先从header中寻找,
// 再从redis中寻找,再从数据库中寻找,
//可自行实现ServiceLancherHandler,本demo只实现了从header中寻找
@Bean
ServiceLancherHandlerChain serviceLancherHandlerChain() {
ServiceLancherHandlerChain chain = new ServiceLancherHandlerChain();
chain.addHandler(new RequestHeaderServiceLancherHandler());
return chain;
}
// AbFilter extends ZuulFilter
// 继承zuulfilter,实现自定义zuul前置拦截器,在服务转发前指定路由策略
// 在自定义的zuulfilter中,获取到第一层转发服务id后,调用指定tag的职责链方法
public Object run() throws ZuulException {
String serviceId = getServiceId();
if (StringUtils.isBlank(serviceId)) {
return null;
}
serviceLancherHandlerChain.handle(serviceId);
return null;
}
2、服务间通过feign调用时转发到指定tag服务关键代码(serviceA\\serviceB\\serviceC\\serviceD)
FeignRibbonFilterInterceptor.class
// 拦截 FeignClient 在feign远程调用前指定调用指定tag的职责链方法,调用后清除
@Pointcut("@within(org.springframework.cloud.openfeign.FeignClient)")
// PS 需实现 RequestInterceptor,将需要的requestHead在feign进行服务间调用时转发到下一级服务,否则服务间调用将损失前端过来的header
FeignHeadersInterceptor.class
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
Map<String, String> headers = HeaderUtil.getHeaders(request);
if (headers!=null && headers.size() > 0) {
Iterator<Entry<String, String>> iterator = headers.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, String> entry = iterator.next();
template.header(entry.getKey(), entry.getValue());
}
}
// 可根据实际业务需求对转发的header进行增减
如何验证
- 启动eurekaserver
- 启动api-gateway
- 启动serviceA\\serviceB\\serviceC\\serviceD(每个服务启动两个实栗,分别使用dev1和dev2的配置文件,通过该参数指定-Dspring.profiles.active=dev1-Dspring.profiles.active=dev2)
- 访问localhost:8761 查看服务是否都已注册上eureka
- 访问http://localhost:8092/service-b/demo/hello,该接口会经由api-gateway服务转发到serviceB->serviceC->serviceD-serviceA,将打印各个服务的tag信息,可观测到每次请求在不同版本的服务中轮询
- 在requestHeader中添加自定义headerribbon-lancher-map:{"service-a":"green","service-b":"blue","service-c":"blue","service-d":"blue"},可使用postman等调用工具,也可使用chrome的[Modify Header Value]插件
- 观测到每次调用链与header中指定的一致
PS: header-key ribbon-lancher-map 定义在RequestHeaderServiceLancherHandler.class中
详细Demo代码:https://download.csdn.net/download/qq_19734597/21454192
以上是关于SpringCloud实战(十七)-基于Ribbon动态路由实现:调用链控制/版本控制/灰度发布(粒度更细,可以控制到每一个微服务模块版本的路由)的主要内容,如果未能解决你的问题,请参考以下文章
SpringCloud Alibaba微服务实战三十七 - Oauth2自定义登录接口
SpringCloud Alibaba微服务实战三十七 - Oauth2自定义登录接口
SpringCloud Alibaba微服务实战三十七 - Oauth2自定义登录接口