最近和组内伙伴一起分享讨论了灰度方案,先将内容整理出来, 具体如下:
灰度方案设计及demo实现
1:微服务系统在启动时将自己注册到服务注册中心,同时对外发布 Http 接口供其它系统调用(一般都是基于Spring MVC)
2:服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对 加了该注解的接口生成动态代理,服务消费者会针对 Feign 生成的动态代理去调用方法时,在底层会生成Http协议格式的请求,类似 /stock/deduct?productId=100
3:Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡 并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成类似调用http接口地址 http://192.168.0.60:9000/stock/deduct?productId=100 最后基于HTTPClient调用请求
基于微服务架构的原理,来设计灰度方案
概要流程:
1.全局配置灰度是否启用–在nacos中配置, 动态更新
2.配置灰度规则, version=2.0 userId=”1234567” productId=”010-1234567”
3.设置灰度服务器, 哪些服务器是灰度服务器。 为其打标签
4.启动所有服务, 服务在nacos上进行注册
5.客户端发起请求, 带着header参数
6.zuul进行过滤,判断是否符合灰度条件, 如果符合,打上灰度标签
7.通过feign将灰度标签进行透传
8.通过ribbon选择跳转的服务器, 可以指定负载均衡策略
9.下一个服务器继续跳转,带上feign的灰度标签,继续请求。
以上是这个灰度方案实现的整体逻辑和思路
二. 组件设计及原理
2.1 灰度的目标
不同的流量过来, 根据元数据匹配, 走不同的微服务
当流量请求过来以后, 根据其匹配的灰度规则的不同, 走的服务有所不同, 可以将其分为三种类型.
- 不匹配任何灰度规则, 则走无灰度服务
- 匹配灰度规则, 则走对应的灰度服务
- 同时匹配多个灰度规则, 选择灰度服务
2.2 管理后台–设置并管理灰度规则
全局灰度标签设置在nacos中, nacos配置的灰度标签的开闭, 可实时自动更新同步.
灰度管理后台, 管理后台主要有两大块内容.
1) 配置灰度规则
1. 根据需要设置灰度规则, 比如: 城市, 用户id, 订单id, 版本号, 学科等
2) 设置灰度服务器
1. 调用nacos接口, 获取所有微服务ip+port
2. 为灰度服务器打灰度标签
3. 做同步策略, 当灰度服务标签内容有变化, 通知网关, 做相应更新2.3. 网关设置–拦截请求, 为其打灰度标签
网关其实就是各种各样的过滤器, 常用的过滤器类型有:pre:前置过滤器, routing: 路由过滤器, post过滤器, error过滤器
这里我们定义一个前置过滤器, 过滤所有 过来的请求, 判断其是否匹配灰度规则
执行步骤:
1:初始化灰度规则, 我们首先判断nacos中灰度规则是否启用, 启用则去灰度管理服务器获取有效的灰度规则
2:判断请求头是否和某一灰度规则匹配, 如果匹配, 则将请求header添加到请求上下文, 后续feign进行透传. 同时添加到ribbon请求上下文, 做服务选择.
2.4. ribbon设置 – 根据灰度规则, 选择灰度服务器
ribbon是客户端负载均衡, 通过对ribbon上下文中的灰度标签和微服务列表中灰度标签的比较, 来选择一台服务器, 作为目标跳转服务器
2.5 自定义Feign拦截器, 实现参数(灰度标签)的透传
feign的实质是拦截器, feign将拦截所有的请求跳转, 主要作用是用来做header参数透传, 保证服务间的调用也可以正确选择灰度服务器.
三. demo设计规划及实现
3.1. 微服务规划
3.2 gateway关键代码实现
/**
* 过滤器执行的内容
* @return
*/
@Override
public Object run() {
// 第一步: 初始化灰度规则
if (!initGray) {
//初始化灰度规则
getGrayRules();
}
// 第二步: 获取请求头(包括请求的来源url和method)
Map<String, String> headerMap = getHeadersInfo();
log.info("headerMap:{},grayRules:{}", headerMap, grayRules);
// 删除之前的路由到灰度的标记
/* if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) {
RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG);
}*/
//灰度开关关闭 -- 无需走灰度, 执行正常的ribbon负载均衡转发策略
if (grayEnable == 0) {
log.info("灰度开关已关闭");
return null;
}
if (!grayRules.isEmpty()) {
for (Map<String, String> grayRuleMap : grayRules) {
try {
// 获取本次灰度的标签,标签的内容是灰度的规则内容
String grayTag = grayRuleMap.get(GrayConstant.GRAY_TAG);
// 第三步: 过滤有效的灰度标签
Map<String, String> resultGrayRuleMap = new HashMap<>();
//去掉值为空的灰度规则
grayRuleMap.forEach((K, V) -> {
if (StringUtils.isNotBlank(V)) {
resultGrayRuleMap.put(K, V);
}
});
resultGrayRuleMap.remove(GrayConstant.GRAY_TAG);
//将灰度标签(规则)小写化
Map<String, String> lowerGrayRuleMap = transformUpperCase(resultGrayRuleMap);
// 第四步: 判断请求头是否匹配灰度规则
if (headerMap.entrySet().containsAll(resultGrayRuleMap.entrySet()) || headerMap.entrySet().containsAll(lowerGrayRuleMap.entrySet())) {
// 这是网关通讯使用的全局对象RequestContext
RequestContext requestContext = RequestContext.getCurrentContext();
// 把灰度规则添加到网关请求头, 后面的请求都可以使用该参数
requestContext.addZuulRequestHeader(GrayConstant.GRAY_HEADER, grayTag);
// 将灰度规则添加到ribbon的上下文
RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, grayTag);
log.info("添加灰度tag成功:lowerGrayRuleMap:{},grayTag:{}", lowerGrayRuleMap, grayTag);
}
} catch (Exception e) {
log.error("灰度匹配失败", e);
}
}
}
return null;
}
这里的逻辑
首先: 获取灰度规则标签. 什么时候获取呢? 第一次请求过来的时候, 去请求灰度标签. 放到全局的map集合中. 后面, 直接拿来就用
第二: 获取请求过来的header, 和灰度规则进行匹配, 如果匹配上了, 那么打灰度标签, 将其灰度请求头添加到请求上下文, 同时添加到ribbon请求的上下文中
接下来, 走feign实现header透传
3.3 feign关键代码实现
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
//处理特殊情况
if (null == ra) {
return;
}
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
//处理特殊情况
if (null == request) {
return;
}
log.info("[feign拦截器] ribbon上下文属性:{}", JSON.toJSONString(RibbonFilterContextHolder.getCurrentContext().getAttributes()));
if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) {
RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG);
}
if (StringUtils.isNotBlank(request.getHeader(GrayConstant.GRAY_HEADER))) {
log.info("灰度feign收到header:{}", request.getHeader(GrayConstant.GRAY_HEADER));
RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, request.getHeader(GrayConstant.GRAY_HEADER));
requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER));
}
}
其实feign的主要作用就是透传, 为什么要透传了呢? 微服务之间的请求, 不只是是首次定向的服务需要进行灰度, 那么后面服务内部相互调用也可能要走灰度, 那么最初请求的请求头就很重要了. 要一直传递下去.
而requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER));就可以实现参数在整个请求进行透传.
请求的参数带好了, 下面就要进行服务选择了, 有n台服务器, 到底要选择哪台服务器呢? 就是ribbon的负载均衡选择了
3.4 ribbon关键代码实现
/**
* 实现父类的负载均衡规则
*
* @param key
* @return
*/
@Override
public Server choose(Object key) {
//return choose(getLoadBalancer(), key);
try {
// 调用父类方法, 获取当前的负载均衡器
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
//获取当前的服务名
String serviceName = loadBalancer.getName();
log.info("[ribbon负载均衡策略] 当前服务名: {}", serviceName);
//获取服务发现客户端
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// 获取指定的服务实例列表
List<Instance> allInstances = namingService.getAllInstances(serviceName);
log.info("[ribbon负载均衡策略] 可用的服务实例: {}", allInstances);
if (allInstances == null || allInstances.size() == 0) {
log.warn("没有可用的服务器");
return null;
}
RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
log.info("MetadataBalancerRule RibbonFilterContext:{}", context.getAttributes());
Set<Map.Entry<String, String>> ribbonAttributes = context.getAttributes().entrySet();
/**
* 服务分为三种类型
* 1. 设置为灰度的服务 --- 灰度服务
* 2. 先设置了灰度, 后取消了灰度的服务 --- 去灰服务
* 3. 普通服务-非灰服务
*/
// 可供选择的灰度服务
List<Instance> grayInstances = new ArrayList<>();
// 非灰服务
List<Instance> noneGrayInstances = new ArrayList<>();
Instance toBeChooseInstance;
if (!context.getAttributes().isEmpty()) {
for (Instance instance : allInstances) {
Map<String, String> metadata = instance.getMetadata();
if (metadata.entrySet().containsAll(ribbonAttributes)) {
log.info("进行灰度匹配,已匹配灰度服务:{},灰度tag为:{}", instance, context.getAttributes().get(GrayConstant.GRAY_TAG));
grayInstances.add(instance);
} else if (!StringUtils.isBlank(metadata.get(GrayConstant.GRAY_TAG))) {
// 非灰度服务
noneGrayInstances.add(instance);
}
}
}
log.info("[ribbon负载均衡策略] 灰度服务: {}, 非灰服务:{}", grayInstances, noneGrayInstances);
// 如果灰度服务不为空, 则走灰度服务
if (grayInstances != null && grayInstances.size() > 0) {
// 走灰度服务 -- 从本集群中按照权重随机选择一个服务实例
toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(grayInstances);
log.info("[ribbon负载均衡策略] 灰度规则匹配成功, 匹配的灰度服务是: {}", toBeChooseInstance);
return new NacosServer(toBeChooseInstance);
}
// 灰度服务为空, 走非断灰的服务
if (noneGrayInstances != null && noneGrayInstances.size() > 0) {
// 走非灰服务 -- 从本集群中按照权重随机选择一个服务实例
toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(noneGrayInstances);
log.info("[ribbon负载均衡策略] 不走灰度, 匹配的非灰度服务是: {}", toBeChooseInstance);
return new NacosServer(toBeChooseInstance);
} else {
log.info("未找到可匹配服务,实际服务:{}", allInstances);
toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(allInstances);
log.info("[ribbon负载均衡策略] 未找到可匹配服务, 随机选择一个: {}", toBeChooseInstance);
return new NacosServer(toBeChooseInstance);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
以上是梳理了网关的主要逻辑思想和关键代码
四. 项目资料
关于以上内容相关的源码,有需要的伙伴可以赞助小张哥喝一杯咖啡,然后在赞赏备注中留下自己的email信息,我会转发code。(code是可以直接运行通过的哦,编译过程中有什么问题可以直接在文章下方给我留言,我会实时通过邮件的方式进行查收的~~)