API网关灰度发布
前面说到Dubbo灰度发布,那网关代理层如何实现灰度发布呢,在网关层实现灰度发布,我们可以采用2种方式实现,分别是权重和灰度规则配置。在这之前我们先了解下Gateway的源码,更利于后面灰度分析。
Gateway源码分析
Gateway在执行路由前会先选择指定的服务,指定的服务选择涉及负载均衡算法,我们先对源码进行剖 析一次,然后再模仿源码编写一个过滤器,让过滤器最后执行。
Gateway负载均衡流程
我们这里只探究SpringCloud Gateway涉及负载均衡中的源码部分,如上图:
1:请求会执行一个过滤器ReactiveLoadBalancerClientFilter,获取一个真实调用的服务实例信息。 2:filter()方法执行过程:
1):获取url
2):调用choose()方法获取要调用的真实实例,服务的IP、端口号、服务名字
3):在url构建出的基础上构建出访问的真实地址 http://192.168.1.103:18081/car
4):进入下一个调用链路
3:choose()方法调用RoundRobinLoadBalancer的choose()方法获取实例。
Gateway负载均衡源码
负载均衡过程中涉及到2个重要对象 ReactiveLoadBalancerClientFilter和 RoundRobinLoadBalancer,我们接下来对这2个对象源码展开分析。
ReactiveLoadBalancerClientFilter
-
ReactiveLoadBalancerClientFilter创建
创建过程中,会初始化一个负载均衡器工厂对象,通过它可以创建负载均衡器 RoundRobinLoadBalancer。 -
filter()方法
filter()方法完成了主要的服务筛选过程,筛选过程如下:
1:调用choose()获取指定实例。
2:真实实例获取后,把用户请求地址替换成真实服务地址信息。 -
choose()
choose()方法调用了LoadBalancerClientFactory工厂对象创建了负载均衡器,真实实例选择其实是在 负载均衡器中完成的。
RoundRobinLoadBalancer
- choose()
该方法只是获取所有有效的实例,并封装成集合对象,然后再调用getInstanceResponse()方法。 - getInstanceResponse()
该方法通过一定算法获取指定实例,并将指定实例封装成DefaultResponse对象,并返回。
网关层灰度发布分析
在网关层实现灰度发布,我们可以采用2种方式实现,分别是权重和灰度规则配置。
1. 权重
当我们的系统刚发布的时候,还不是很稳定,可以放一部分流量来测试新系统,微服务网关可以采用权 重的方式实现流量控制,如果新系统没有问题,逐步把老系统换成新系统。
2. 灰度规则配置
灰度规则配置则是网关层常用的灰度发布方式,实现的流程我们分析一下:
1:用户请求到达微服务网关
2:微服务网关执行拦截
3:加载规则配置
4:按照规则配置适配服务
5:执行链路调用
规则配置比较灵活,没有绝对方案,常用的有 区域IP切流 、 固定用户切流 、 指定版本切流 。
区域IP切流: 指定省份或城市的IP使用灰度版本,多用于多城市子站点发布
固定用户切流: 部分内部账号或测试账号或部分指定会员使用灰度版本
指定版本切流: 根据用户当前版本使用指定服务,用户版本信息一般会传递到后台,多用于APP灰度发 布
3. 灰度发布-权重分流
基于Gateway实现灰度发布,首先要做的就是拦截用户请求,再根据规则路由请求。
项目结构:
权重分流结构
如上图,加入 carv1是稳定版, carv2是测试版本,我们为了测试 carv2是否稳定,可以放部分流量 到 carv2来测试,放部分流量可以采用权重的方式来实现,让 carv1占据90%流量, carv2只占据 10%流量。
权重配置只需要做2个操作:
1:让相关服务归属一个服务组
2:给服务组配置权重
修改 gray-gateway的 bootstrap.yml配置文件,如下:
配置代码如下:
gateway:
routes:
# 通过访问http://localhost:8001/car,来测试权重 - id: car-version1
uri: lb://car-version1
predicates:
- Path=/car/**
- Weight=group1, 8
- id: car-version2
uri: lb://car-version2
predicates:
- Path=/car/**
- Weight=group1, 2
多策略灰度发布
多策略灰度发布实现分析
如上图,实现多策略灰度发布,可以按照这个流程来实现:
1:稳定系统和灰度系统将信息注册到Nacos中,包括版本信息
2:用户执行请求,我们需要根据灰度规则动态筛选指定服务,可以编写过滤器拦截所有请求,再执行服务筛 选
3:服务筛选方法在ReactorServiceInstanceLoadBalancer对象中,叫choose(),我们需要重写它
4:根据当前用户请求信息、IP信息、用户信息进行不同策略的灰度服务筛选
5:筛选后,需要将筛选的服务返回给过滤器,过滤器执行链路调用
多策略主流程实现
我们先把主要运行流程代码实现出来,再编写不同的策略进行切换即可。主流程我们按照如下流程实现 操作:
1:配置服务节点注册到nacos中的版本信息
2:配置服务调用的scheme
3:获取scheme,bootstrap.yml中 uri: scheme://xxx
4:判断当前scheme是否为灰度服务,我们定义灰度服务的scheme为grayLb
5:根据不同策略获取灰度服务实例
6:封装灰度实例地址信息(将 http://car-version1/car 换成
http://192.168.211.1:18082/car)
7:灰度服务链路调用
version和scheme配置
修改 carv1和 carv2的bootstrap.yml,如下操作:
修改 gray-gateway的bootstrap.yml
上图代码如下:
gateway:
routes:
# 通过访问http://localhost:8001/car,来测试权重 - id: car-version1
uri: lb://car-version
predicates:
- Path=/car/**
- Weight=group1, 8
- id: car-version2
uri: lb://car-version
predicates:
- Path=/car/**
- Weight=group1, 2
#灰度版本路由配置
- id: car-gray
uri: grayLb://car-version
predicates:
- Path=/api/** filters:
- StripPrefix=1
灰度服务实例获取
基于API网关灰度发布需要拦截用户请求,按照业务需求实现实例服务选择,我们可以使用拦截器拦截 GrayFilter,实现 ReactorServiceInstanceLoadBalancer自定义实例选择规则,由于API网关实现 灰度操作会涉及大量代码,每次添加代码我们都用关系图展示出来,更有助于分析,代码关系图如下:
我们首先准备一个过滤器 GrayFilter,过滤器中要调用负载均衡器,所以需要 创建2个对象,分别为 LoadBalancerClientFactory和 LoadBalancerProperties,过滤器代码如 下:
@Configuration
public class GrayFilter implements GlobalFilter, Ordered {
//负载均衡构建工厂对象
private LoadBalancerClientFactory clientFactory;
//负载均衡属性对象
private LoadBalancerProperties properties;
public GrayFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
this.clientFactory = clientFactory;
this.properties = properties;
}
/*****
* 拦截所有请求
* @param exchange:用户请求响应的所有对象
* @param chain:链路调用对象
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//uri中以grayLb开始的路由都是灰度系统调用
//灰度系统调用->调用LoadBalancer的choose()方法获取实例信息
//灰度系统调用->将用户请求的url地址中的域名换成真实服务的IP、端口号
//灰度系统调用->调用下一个过滤器链路
//非灰度系统请求,直接调用下一个过滤器链路
return chain.filter(exchange);
}
//最后执行
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
我们修改 gray-gateway,后面所有操作都直接操作 gray-gateway服务,添加负载均衡实例筛选对象 GrayLoadBalancer取代默认的负载均衡筛选操作,我们先把该类创建完成, 筛选实例逻辑根据不同策略一步一步实现,代码如下:
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
/*****
* 根据不同策略选择指定服务实例
* @param request:用户请求信息封装对象
* @return
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) { return null;
}
因为我们需要在 GrayLoadBalancer中从服务集合中筛选一个符合调用的服务实例,所以每次需要把服 务集合传到 GrayLoadBalancer中,在 GrayLoadBalancer中创建带参构造函数,创建 ServiceInstanceListSupplier集合对象,该对象封装了单个服务实例,代码如下:
我们需要在 GrayFilter过滤器中调用真实实例,可以在 GrayFilter编写一个choose方法调用 GrayLoadBalancer中的 choose方法,关系图如下:
修改GrayFilter添加方法调用 GrayLoadBalancer.choose()方法来获取指定 服务实例信息,该方法会用到 LoadBalancerClientFactory加载服务列表, LoadBalancerProperties可以动态设置服务属性(等会会用到),代码如下:
/****
* 调用GrayLoadBalancer中的choose()方法获取服务实例
* @return
*/
public Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange){ //获取Uri
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
//创建GrayLoadBalancer
GrayLoadBalancer grayLoadBalancer = new GrayLoadBalancer(
clientFactory.getLazyProvider(uri.getHost(),
ServiceInstanceListSupplier.class),//服务集合封装对象
uri.getHost()//服务名字
);
//调用GrayLoadBalancer.choose() IP、ID(header)
HttpHeaders headers = exchange.getRequest().getHeaders();
return grayLoadBalancer.choose(new DefaultRequest<HttpHeaders>(headers));
}
服务真实地址更换
我们在Gateway的配置文件中配置地址是 lb://car-version,加上用户访问地址后是 lb://car- version/api/car,但这并不是真实地址,我们需要把它换成真实地址,也就是
http://192.168.211.1/api/car,我们在 GrayFilter中创建一个方法,实现地址替换,
实现代码如下:
/****
* 将请求地址中的域名换成真实服务的IP、端口号
* @return
*/
public Mono<Response<ServiceInstance>>
setInstanceInfo(Mono<Response<ServiceInstance>>
serviceInstanceResponse,ServerWebExchange exchange){
return serviceInstanceResponse.doOnNext(new
Consumer<Response<ServiceInstance>>() {
@Override
public void accept(Response<ServiceInstance> serviceInstance) { //获取url
URI uri = exchange.getRequest().getURI();
//获取真实服务地址信息 IP、端口
DelegatingServiceInstance delegatingServiceInstance = new
DelegatingServiceInstance(serviceInstance.getServer(),null);
//将用户请求的url地址换成真实IP、端口
URI requestURI =
LoadBalancerUriTools.reconstructURI(delegatingServiceInstance, uri);
//将requestURI添加到exchange的属性中
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR,re questURI);
}
});
}
主流程实现
所有的主流程已经编写完成,我们可以编写流程调用了,在 GrayFilter中的 filter方法中执行如下 调用:
/*****
* 拦截所有请求
* @param exchange:用户请求响应的所有对象
* @param chain:链路调用对象
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取scheme,bootstrap.yml中 uri: scheme://xxx
URI uri =
exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix =
exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
//判断当前scheme是否为灰度服务,我们定义灰度服务的scheme为grayLb
if(uri != null && ("grayLb".equals(uri.getScheme()) ||
"grayLb".equals(schemePrefix))){
//根据不同策略获取灰度服务实例
Mono<Response<ServiceInstance>> responseInstance =
this.choose(exchange);
//封装灰度实例地址信息(将 http://car-version1/car 换成
http://192.168.211.1:18082/car)
responseInstance = setInstanceInfo(responseInstance,exchange);
//灰度服务链路调用
return responseInstance.then(chain.filter(exchange));
}
return chain.filter(exchange);
}
灰度系统权重分流
用户访问灰度系统的时候,灰度系统也不一定是单个节点,我们很多时候也需要做权重分流,机器性能 好的服务器处理流量多,机器性能弱的服务器处理流量少。
我们假设一个场景: carv1和 carv2都是灰度系统,在他们两个系统之间实现权重控制,该如何做 呢?
自定义权重需要配置Nacos元数据权重节点,在程序中获取元数据权重,并计算权重 getServiceInstanceResponseWithWeight,计算权重可以使用成熟的工具 `WeightMeta和 WeightRandomUtils,对象方法关系图如下:
自定义权重分析
修改GrayLoadBalancer添加权重实例筛选方法
/******
* 权重选择一个实例
* @return
*/
public Response<ServiceInstance> weight(List<ServiceInstance> instances){ //封装Map<ServiceInstance,Integer> 实例:权重
Map<ServiceInstance,Integer> weightMap = new HashMap<ServiceInstance, Integer>();
for (ServiceInstance instance : instances) {
//获取元数据中的权重
Map<String, String> metadata = instance.getMetadata();
Integer serviceWeight =Double.valueOf( metadata.get("nacos.weight")
).intValue();
//存储到weightMap中
weightMap.put(instance,serviceWeight);
}
//将服务权重Map对象封装成WeightMeta对象
WeightMeta<ServiceInstance> weightMeta =
WeightRandomUtils.buildWeightMeta(weightMap);
//随机选择一个服务
ServiceInstance serviceInstance = weightMeta.random();
return serviceInstance==null? new EmptyResponse() : new DefaultResponse(serviceInstance);
}
权重筛选调用
在 GrayLoadBalancer中创建 getInstanceResponse方法,用于调用不同策略方法获取指定服务实 例,并在 choose方法中实现对 getInstanceResponse的调用,代码如下:
/*****
* 根据不同策略选择指定服务实例
* @param request:用户请求信息封装对象
* @return
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
//获取所有请求头
HttpHeaders headers = (HttpHeaders) request.getContext();
//服务列表不为空
if (this.serviceInstanceListSupplierProvider != null) {
//获取有效的实例对象
ServiceInstanceListSupplier supplier =
this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListS upplier::new);
//获取有效实例集合
Mono<List<ServiceInstance>> next = supplier.get().next();
//按照指定路由规则查找符合的实例对象
return supplier.get().next().map(list->getInstanceResponse(list));
}
return null;
}
/******
* 获取实例
* @param instances
* @return
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
//找不到实例
if (instances.isEmpty()) {
return new EmptyResponse();
}else{
//权重路由
return weight(instances);
}
}
- 初始化配置
创建 GrayBalanceConfig执行初始化配置:
@Configuration
public class GrayBalanceConfig {
/****
* 过滤器配置
*/
@Bean
@ConditionalOnMissingBean({GrayFilter.class})
public GrayFilter grayFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
return new GrayFilter(clientFactory, properties);
}
}
- 测试
启动 carv1和 carv2以及 gray-gateway,并访问 http://localhost:8001/api/car,可以看到访问 结果会出现如下结果,并且出现比例为1:1 ,权重比例可以在Nacos中调整
Nacos中权重调整
调整后服务调用比例为6:1
灰度发布版本分流
版本灰度发布一般适用于App,用户每次请求后台会将版本号携带过来,版本号可以在每次向后台发起 请求的时候把版本号一起塞到请求头中,后台服务获取请求头中的版本信息,再根据版本信息匹配 Nacos中注册的服务元数据的版本号,如果匹配上了,则进行服务权重筛选找到最符合的服务实例信 息。
在微服务网关这里实现版本分流,我们可以先在程序配置文件中向Nacos元数据写入版本号,并在实例 选择出创建方法 getServiceInstanceResponseByVersion获取Nacos中的版本,指定当前服务需要调 用的版本,并按照权重规则下沉调用,关系图如下:
- 版本号路由筛选
在 GrayLoadBalancer中添加版本筛选方法,确认版本后,多个版本的服务可能是集群,因此还要做权 重筛选,代码如下:
/***
* 版本号选择
*/
public Response<ServiceInstance> version(List<ServiceInstance> instances,String version){
//存储所有有效服务
List<ServiceInstance> serviceInstances = new ArrayList<ServiceInstance>(); //循环所有服务
for (ServiceInstance instance : instances) {
//对比元数据中是否包含版本号信息
Map<String, String> metadata = instance.getMetadata();
//如果包含,则将服务添加到被调用的服务集合中
for (Map.Entry<String, String> entry : metadata.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if(key.equals("version") && value.equals(version)){ serviceInstances.add(instance);
continue;
}
}
}
//根据权重选择
return weight(serviceInstances);
}
在 GrayLoadBalancer.choose和 GrayLoadBalancer.getInstanceResponse中分别把请求头信息传 递过来:
GrayLoadBalancer.choose:
return supplier.get().next().map(list->getInstanceResponse(list,headers));
GrayLoadBalancer.getInstanceResponse:
/******
* 获取实例
* @param instances
* @return
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
//找不到实例
if (instances.isEmpty()) {
return new EmptyResponse();
}else{
//获取版本号
String versionNo = headers.getFirst("version");
//权重路由、 根据版本号+权重路由
return StringUtils.isEmpty(versionNo)? weight(instances) :
version(instances,versionNo);
}
}
- 测试
拷贝 carv2一份,改名 carv3,端口 18083, version=v2,进行测试,效果如下:
此时测试,只会调用 carv2和 carv3服务。