简介
敏捷开发迭代周期短发布快,每周都可能面临版本发版上线,为最大可能的降低对用户的影响提高服务可用率,大部分团队都需要等到半夜做发布和支持。本文就如何基于spring cloud体系做灰度发版改造提供了方案,让我们终于白天也能偷偷摸摸的无感知发版,验证,上线等动作,从此再也不用因为发版要熬夜了。
本文阐述的方案是灰度发版方案的一种实现(各种部署方案可参考文档最后的附录),属于一种比较节约资源的部署方案,通过精准的导流和开关控制实现用户无感知的热部署,比较适合中小企业采纳应用。整体技术架构基于nepxion discovery插件结合项目中各个实践场景做了方案说明和源代码展示,如需要做权重,分组等策略可自行扩展。
术语与配置
名称 | 说明 |
---|---|
灰度节点 | 被标记为灰度的节点 |
灰度入口 | 前端部署的节点被标记为灰度的节点 |
灰度用户 | 账号被标记位灰度的用户 |
灰度流量 | 路由是需要优先选择灰度节点的请求链 |
灰度开关 | 是否开启灰度路由,值为:开启/关闭 |
灰度流量开关 | 是否所有流量都是灰度流量,值为开启/关闭 |
开关与流量关系
灰度流量开关\灰度总开关 | 适用场景 | 正常用户 正常入口 |
灰度用户 灰度入口 |
---|---|---|---|
(灰度总开关)开 (灰度流量开关)关 |
灰度节点发版,新版本验证阶段 | 旧版本体验 | 新版本体验 |
(灰度总开关)开 (灰度流量开关)开 |
正常节点发版,新版本批量部署阶段 | 新版本体验 | 新版本体验 |
(灰度总开关)关 (灰度流量开关)开/关 |
新版本完成上线 | 新版本体验 | 新版本体验 |
灰度配置 Gray Properties
![1591942259553](d:\user\01388368\Application Data\Typora\typora-user-images\1591942259553.png)
用户白名单:
节点清单加载可以从eureka获取
public ResultMessage getServices() {
//本地配置的服务map
Map<String, Service> servicesLocalMap = getServicesLocalMap();
//要返回的服务清单
List<Service> services = new ArrayList();
discoveryClient.getServices().forEach(service -> {
final List<Instance> instances = new ArrayList();
discoveryClient.getInstances(service).forEach(instanceInfo -> {
instances.add(toInstance(instanceInfo));
});
//优先使用本地实例
if (null != servicesLocalMap.get(service)) {
final List<Instance> serviceLocalInstances = servicesLocalMap.get(service).getData();
//更新状态
List<Instance> serviceLocalInstancesHasLatestStatus = serviceLocalInstances.stream()
.map(instanceLocal -> instances.stream()
.filter(instance -> StringUtils.join(instance.getHost(), instance.getPort().toString()).equals(StringUtils.join(instanceLocal.getHost(), instanceLocal.getPort().toString())))
.findFirst().map(m -> {
instanceLocal.setStatus(m.getStatus());
return instanceLocal;
})
// .orElse(null)
.orElseGet(() -> {
instanceLocal.setStatus("OFFLINE");
return instanceLocal;
})
).filter(Objects::nonNull).collect(Collectors.toList());
//去除eureka中本地已配置的实例
List<Instance> instancesRemoveLocal = instances.stream().filter(instance -> !serviceLocalInstancesHasLatestStatus.stream()
.anyMatch(instanceLocal -> StringUtils.join(instance.getHost(), instance.getPort().toString()).equals(StringUtils.join(instanceLocal.getHost(), instanceLocal.getPort().toString()))))
.collect(Collectors.toList());
//清空并重新添加处理过的本地和远程实例
instances.clear();
instances.addAll(instancesRemoveLocal);
instances.addAll(serviceLocalInstancesHasLatestStatus);
//本地服务实例排除已经添加的服务
servicesLocalMap.remove(service);
}
//排序
Collections.sort(instances);
//单个服务和节点添加
Service serviceResp = new Service();
serviceResp.setService(service);
serviceResp.setData(instances);
services.add(serviceResp);
});
//添加eureka中不存在,本地存在的服务
servicesLocalMap.values().forEach(service->{
Collections.sort(service.getData());
services.add(service);
});
Collections.sort(services);
return ResultCode.SUCCESS.withData(services);
}
依赖 Gray Dependency
<!-- 服务的灰度依赖 -->
<dependency>
<groupId>com.sf</groupId>
<artifactId>cloud-discovery-service-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 网关的灰度依赖,注意不能与服务的灰度依赖一起配置 -->
<dependency>
<groupId>com.sf</groupId>
<artifactId>cloud-discovery-gateway-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 灰度发版插件 -->
<plugin>
<groupId>com.sf</groupId>
<artifactId>maven-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>gray-plugin</goal>
</goals>
<configuration>
<grayBuildLocationExclude></grayBuildLocationExclude>
</configuration>
</execution>
</executions>
</plugin>
灰度头部 GrayHeader
灰度头部信息信息,编码支持的类有 < GrayHeader,GrayHeaderConstant,GrayUtil, ServiceGrayUtil >
灰度对象存放的上下文有:自定义实现类 < GrayHeaderHolder >,Request内置实现类 < RequestContextHolder >
参数key | 参数value | 说明 |
---|---|---|
h-gray-is | true/false | 是否为灰度流量 |
h-gray-domain | 域名 | 用户请求的域名 |
h-gray-userid | xxx | 用户请求的账户 |
技术改造点
灰度改造分三大类:网关改造,服务改造,场景改造。主要目的是实现灰度头部计算、复用、续传,负载均衡的改造。
网关改造 cloud-discovery-gateway-starter
修改pom.xml依赖
<dependency>
<groupId>com.nepxion</groupId>
<artifactId>discovery-plugin-starter-eureka</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.nepxion</groupId>
<artifactId>discovery-plugin-strategy-starter-gateway</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>
configure加载项
@Configuration
public class DiscoveryGatewayAutoConfiguration {
//负载均衡改造注入
@Bean
public DiscoveryEnabledAdapter discoveryEnabledAdapter() {
return new GatewayGrayDiscoveryEnabledAdapter();
}
//灰度路由计算注入
@Bean
public GrayRouteFlagFilter grayRouteFilter() {
return new GrayRouteFlagFilter();
}
//灰度配置获取注入
@Bean
public GrayPropertiesLoader grayPropertiesLoader() {
return new GrayPropertiesLoader(gatewayRedisson);
}
}
[路由场景]增加header
在网关新增Filter,将request上下文和灰度配置匹配,算出灰度路由标记
public class GrayRouteFlagFilter implements GlobalFilter, Ordered {
@Autowired
private GrayPropertiesLoader grayPropertiesLoader;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpRequest request = exchange.getRequest();
Boolean isGray = grayPropertiesLoader.calculateGrayFlag(request.getHeaders().getFirst(GrayHeaderConstant.GRAY_IS),
request.getHeaders().getFirst(GrayHeaderConstant.GRAY_NGINX_IP),
request.getHeaders().getFirst(GrayHeaderConstant.GRAY_USER_ACCOUNT));
request = request.mutate().header(GrayHeaderConstant.GRAY_IS, isGray.toString()).build();
exchange = exchange.mutate().request(request).build();
}catch (Throwable e){
log.error("未知错误",e);
}finally {
return chain.filter(exchange);
}
}
}