最近做了一个微服务的全链路灰度发布的需求,开始完全没思路,经过一段时间的调研以及源码阅读在加上很多优秀博客的分享成功完成需求,趁着周末做一下复盘和记录。
正文
一.首先介绍一下灰度发布
1.什么是灰度发布(百度百科)
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。
在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
灰度期:灰度发布开始到结束期间的这一段时间,称为灰度期。
2.灰度发布好处
1.降低发布影响面: 就算出问题,也只会影响部分用户,从而可以提前发现新版本中的 bug,然后在下一次发布前提前修复,避免影响更多用户;
2.提升用户体验: 除了能发现 bug,还能很好的收集新版本的用户使用反馈,从而提前调整系统,提升用户体验,也能给后续的产品演进带来参考价值。
3.可以做到不停机的热迁移,版本回滚便捷(速度快)
3.灰度发布类型以及选型
二.灰度发布具体实现
整体大致分为三步,第一步是染色规则的管理,第二步网关全局过滤器进行染色,第三步灰度标记透传以及根据灰度标记进行负载均衡
1.染色规则管理
1.微服务注册到注册中心时在元数据中增加version:versionX的数据,一遍后续在负载均衡时对服务实例进行筛选
2.染色规则的增删改查,这里包含两大类型。
前提是一下两种是互斥的,一个服务配置了规则就不能配置百分比了。
- 各种规则组合
入参Header、Cookie、Param三类中的入参,比如userId、host或者某些特殊key - 百分比
根据百分比去访问某个服务的不同版本实例,这里和网关原生的根据权重访问不同服务是一致的。
2.网关全局过滤器进行染色
这里需要继承全局过滤器,实现一个自定义的全局过滤器,内部可以获取所有的染色规则以及入参,然后对所有服务进行染色,并最终将其封装到一个灰度map放入到请求头中,在整个调用链路中透传。
大致思路是这样,具体实现可以把灰度规则抽成一个服务注入到这里,详细的染色过程就见仁见智了,自己实现即可,另外这段代码是问的chagpt。
public class GlobalGrayFilter implements GlobalFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//① 解析请求头,查看是否存在灰度发布的请求头信息,如果存在则将其放置在ThreadLocal中
HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.containsKey(GrayConstant.GRAY_HEADER)){
String gray = headers.getFirst(GrayConstant.GRAY_HEADER);
if (StrUtil.equals(gray,GrayConstant.GRAY_VALUE)){
//②设置灰度标记
GrayRequestContextHolder.setGrayTag(true);
}
}
//③ 将灰度标记放入请求头中
ServerHttpRequest tokenRequest = exchange.getRequest().mutate()
//将灰度标记传递过去
.header(GrayConstant.GRAY_HEADER,GrayRequestContextHolder.getGrayTag().toString())
.build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return chain.filter(build);
}
}
3.灰度标记透传以及根据灰度标记进行负载均衡
不论是网关调用服务,还是后续微服务之间调用都涉及服务的远程调用,这里有两种方式openfeign和resttemplate,他们底层负载均衡有两种一种是ribbon(很不幸,这个在springcloud中已经停止运维了,所以为了不可预见的坑,项目组果断放弃了),另一种是loadbalancer,这里只需要重写一个反应式的loadbalancer即可,里面使用了生产者的方式来获取微服务实例和注册中心解绑。
- 透传灰度标记
以下是最常见的实现方式,具体自己项目需要调整。
- openfeign
在OpenFeign中,我们可以自定义一个RequestInterceptor来实现透传。这里我们以透传用户的上下文信息为例,具体实现代码如下:
@Component
public class FeignUserContextInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
UserContext userContext = UserContextHolder.getUserContext();
if (userContext != null) {
requestTemplate.header("Correlation-Id", userContext.getCorrelationId());
requestTemplate.header("User-Id", userContext.getUserId());
requestTemplate.header("Auth-Token", userContext.getAuthToken());
}
}
}
- resttemplate
@Component
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
UserContext userContext = UserContextHolder.getUserContext();
if (userContext != null) {
request.getHeaders().add("Correlation-Id", userContext.getCorrelationId());
request.getHeaders().add("User-Id", userContext.getUserId());
request.getHeaders().add("Auth-Token", userContext.getAuthToken());
}
return execution.execute(request, body);
}
}
- 路由选择
这里就非常简单了,只要继承ReactorServiceInstanceLoadBalancer自定义自己的功能即可。
从注册中心获取微服务,从请求头中获取灰度标记,对现有微服务进行筛选,然后对剩下的服务实例进行负载均衡,负载均衡算法包括轮询或者随机,具体的可以再增加。这里代码简单就不放代码了。
这是全链路灰度发布的大致思路及部分详细过程,如有不同意见欢迎随时讨论或指正。