1、什么是灰度发布
灰度发布, 也叫金丝雀发布。是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。
2、常见灰度发布策略
策略一:基于客户端公网ip判断是否需要走灰度环境。
策略二:基于用户判断是否走灰度环境。
3、常见实现方式
对于前后端分离的项目,一般通过nginx 转发解决跨域问题。
3.1 只有服务需要灰度环境
当前端没有修改或者修改不影响逻辑的时候,基本只要控制服务端有灰度环境,这种情况实现如下:
1、通过nginx+lua+redis,判断是否走灰度环境,其中redis用户存储走灰度环境用,lua在nginx 里面读取用户id,并且获取客户端请求用户id(一般通过请求头获取),通过比较判断,将在reids存储的用户转发到灰度环境网关或者服务端。(注意灰度环境的服务和正式服务注册中心不是同一个或者网络不是同一个)
2、通过服务网关辨别是否转发到灰度服务端。以SpringCloudGateway作为网关框架,nacos作为注册中、为例,具体实现方式:
定义一个灰度标记Holder
public class GrayEnvHolder {
private static final ThreadLocal<EnvEnum > grayFlag = new ThreadLocal<>();
public static void setGrayEnvTag(final EnvEnum tag) {
grayFlag.set(tag);
}
public static EnvEnum getGrayEnvTag() {
return grayFlag.get();
}
public static void remove() {
grayFlag.remove();
}
}
定义一个过滤器,确认是否要走灰度环境,代码如下
public class GrayGatewayBeforeFilter implements GlobalFilter, Ordered {
@Autowired
private GrayGatewayProperties grayGatewayProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
EnvEnum envEnum = EnvEnum.PROD
// 当灰度开关打开时才进行请求头判断
if (grayGatewayProperties.getEnabled()) {
// 判断是否需要调用灰度版本
if (checkGray(exchange.getRequest())) {
envEnum = EnvEnum.GRAY;
}
}
GrayEnvHolder.setGrayTag(envEnum );
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header(GrayConstant.GRAY_HEADER, grayStatusEnum.getVal())
.build();
ServerWebExchange newExchange = exchange.mutate()
.request(newRequest)
.build();
return chain.filter(newExchange);
}
/**
* 校验是否使用灰度版本
*/
private boolean checkGray(ServerHttpRequest request) {
if ( checkGrayIPList(request)) {
return true;
}
return false;
}
private boolean checkGrayIPList(ServerHttpRequest request) {
// 读取走灰度环境ip
List<String> grayIPList = grayGatewayProperties.getGrayIPList();
if (CollectionUtils.isEmpty(grayIPList)) {
return false;
}
String realIP = request.getHeaders().getFirst("X-Real-IP");
if (realIP == null || realIP.isEmpty()) {
realIP = request.getRemoteAddress().getAddress().getHostAddress();
}
if (realIP != null && CollectionUtils.contains(grayIPList.iterator(), realIP)) {
return true;
}
return false;
}
@Override
public int getOrder() {
// 设置过滤器的执行顺序,值越小越先执行
return Ordered.HIGHEST_PRECEDENCE;
}
}
然后就要通过路由重写,把不同用户转发到不同的环境。我们知道Springboot 项目负载均衡路由有Ribbon实现。它有它默认路由算法。所以我们要重新它即可。此时定义灰度环境版本变为v2:代码如下:
public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
@Autowired
private GrayVersionProperties grayVersionProperties;
@Value("${spring.cloud.nacos.discovery.metadata.version}")
private String metaVersion;
public List<Server> getReachableServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return new ArrayList<>();
}
List<Server> reachableServers = lb.getReachableServers();
return getGrayServers(reachableServers);
}
/**
* 所有已知的服务器,可访问和不可访问,并对灰度标识进行判断
*/
public List<Server> getAllServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return new ArrayList<>();
}
List<Server> allServers = lb.getAllServers();
return getGrayServers(allServers);
}
protected List<Server> getGrayServers(List<Server> servers) {
List<Server> result = new ArrayList<>();
if (servers == null) {
return result;
}
String currentVersion = metaVersion;
EnvEnum envEnum = GrayFlagRequestHolder.getGrayTag();
if (envEnum != null) {
switch (envEnum ) {
case PROD:
currentVersion = grayVersionProperties.getProdVersion();
break;
case GRAY:
currentVersion = grayVersionProperties.getGrayVersion();
break;
}
}
for (Server server : servers) {
NacosServer nacosServer = (NacosServer) server;
Map<String, String> metadata = nacosServer.getMetadata();
String version = metadata.get("version");
// 判断服务metadata下的version是否于设置的请求版本一致
// 注意灰度环境和生产正式环境接口版本不一样
if (version != null && version.equals(currentVersion)) {
result.add(server);
}
}
return result;
}
}
网关转发到到灰度环境要移除此线程holder,因此要定义后置拦截器。代码省略。网关转发后就到应用存面,考虑到微服务之间调用,因此要实现灰度标记的传递,通常是通过header传递。
public class GrayFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 如果灰度标记存在,将灰度标记通过HttpHeader传递下去
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String envEnum = request.getHeader("Gray");
if (envEnum != null ) {
template.header(envEnum );
}
}
}
当然每个微服务都要重新路由。参数上方的代码即可。方式二灰度环境和正式环境可以部署在同一个网络已经同一个nacos(同一个空间)
3.2 前端和服务端都需要灰度环境
当前端页面改动也很大时候,或者由于业务需要或者测试需要也要将前端发布灰度环境。首先前端需要打一个灰度环境包
实现方式如下:
1 、实现方式 可以参照3.1中方式1,只需要将灰度的前端放指定目录,修改nginx配置即可。
此时服务转发可以直接修改nginx 到网关和微服务转发配置,也可以参照3.1中方式2实现。建议修改nginx配置,这种方式比较简洁、改动比较少