基于Ribbon动态路由实现:调用链控制/版本控制/灰度发布
demo地址:https://gitee.com/kenwar/ribbon-chain-control-demo
有什么用
- 可实现一套调用链管理工具,将管理好的调用链保存于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保护模式导致应用无法下线问题
- 其他玩法请大胆想象...
灰度发布图示:
实现原理及实战
实现原理: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);
}
- 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;
}
- 服务间通过feign调用时转发到指定tag服务关键代码(serviceAserviceBserviceCserviceD)
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
- 启动serviceAserviceBserviceCserviceD(每个服务启动两个实栗,分别使用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中