什么是灰度发布?

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

本文以springcloud gateway + nacos来演示如何实现灰度发布,如果对springcloud gateway和nacos还不熟悉的朋友,可以先阅读如下文章,然后再阅读本文。

springcloud gateway官方介绍

nacos官方介绍

实现的整体思路:

  • 编写带权重的灰度路由
  • 编写自定义filter
  • nacos服务配置需要灰度发布的服务的元数据信息以及权重
  • 灰度路由从nacos服务拉取元数据信息以及权重,然后根据权重算法,返回符合要求的服务实例给自定义的filter
  • ​网关配置文件配置需要灰度路由的服务(因为本文代码没有网关实现动态路由,不然灰度路由可以配置在配置中心,从配置中心拉取)​
  • filter通过责任链模式,把服务实例透传给其他filter比如NettyRoutingFilter

下边进入实战

正文

1、所使用的开发版本

    <jdk.version>1.8</jdk.version>
<!-- spring cloud -->
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>

2、pom.xml引入

   <dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency> <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency> <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency> </dependencies>

ps:nacos的jar注意排除ribbon依赖,不然loadbalancer无法生效

3、编写权重路由

 public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(GrayLoadBalancer.class);
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private String serviceId; public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
} @Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
if (this.serviceInstanceListSupplierProvider != null) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers));
} return null; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
if (instances.isEmpty()) {
return getServiceInstanceEmptyResponse();
} else {
return getServiceInstanceResponseWithWeight(instances);
}
} /**
* 根据版本进行分发
* @param instances
* @param headers
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
String versionNo = headers.getFirst("version");
System.out.println(versionNo);
Map<String,String> versionMap = new HashMap<>();
versionMap.put("version",versionNo);
final Set<Map.Entry<String,String>> attributes =
Collections.unmodifiableSet(versionMap.entrySet());
ServiceInstance serviceInstance = null;
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
if(metadata.entrySet().containsAll(attributes)){
serviceInstance = instance;
break;
}
} if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
return new DefaultResponse(serviceInstance);
} /**
*
* 根据在nacos中配置的权重值,进行分发
* @param instances
*
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) {
Map<ServiceInstance,Integer> weightMap = new HashMap<>();
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
System.out.println(metadata.get("version")+"-->weight:"+metadata.get("weight"));
if(metadata.containsKey("weight")){
weightMap.put(instance,Integer.valueOf(metadata.get("weight")));
}
}
WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap);
if(ObjectUtils.isEmpty(weightMeta)){
return getServiceInstanceEmptyResponse();
}
ServiceInstance serviceInstance = weightMeta.random();
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
System.out.println(serviceInstance.getMetadata().get("version"));
return new DefaultResponse(serviceInstance);
} private Response<ServiceInstance> getServiceInstanceEmptyResponse() {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}

4、自定义filter

public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {

    private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class);
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
private LoadBalancerProperties properties; public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
this.clientFactory = clientFactory;
this.properties = properties;
} @Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
} @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
} return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
} DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme);
URI requestUrl = this.reconstructURI(serviceInstance, uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
} exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
}).then(chain.filter(exchange));
} else {
return chain.filter(exchange);
}
} protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
} private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + uri.getHost());
} else {
return loadBalancer.choose(this.createRequest(exchange));
}
} private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
Request<HttpHeaders> request = new DefaultRequest<>(headers);
return request;
}
}

5、配置自定义filter给spring管理

@Configuration
public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {
public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() {
} @Bean
@ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);
} }

6、编写网关application.yml配置

server:
port: 9082
# 配置输出日志
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG #开启端点
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: gateway-reactor-gray
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: hello-consumer
uri: grayLb://hello-consumer
predicates:
- Path=/hello/**

uri中的grayLb配置,代表该服务需要进行灰度发布​

7、在注册中心nacos配置灰度发布的服务版本以及权重值

weight代表权重,version代表版本​

总结

上述就是实现灰度发布的过程,实现灰度发布的方法有很多种,文章中只是提供一种思路。虽然springcloud官方推荐使用loadbalancer来代替ribbon。因为ribbon是阻塞的,但从官方的loadbalancer的负载均衡算法来看,目前loadbalancer默认只支持轮询算法,要其他算法得自己扩展实现,而ribbon默认支持7种算法,用默认的算法基本上就可以满足我们的需求了。其次ribbon支持懒加载处理,超时以及重试与断路器hystrix集成等配置,loadbalancer目前就支持重试。所以如果正式环境要自己实现灰度发布,可以考虑对ribbon进行扩展。本文的实现只是作为一种扩展补充,毕竟springcloud推荐loadbalancer,索性就写个demo实现下。

最后灰度发布的实现,业内也有开源的实现--Discovery,感兴趣的朋友可以通过如下链接进行查看

https://github.com/Nepxion/Discovery

demo链接

https://github.com/lyb-geek/gateway

基于springcloud gateway + nacos实现灰度发布(reactive版)的更多相关文章

  1. 基于nginx+lua简单的灰度发布系统

    upstream.conf upstream grey_1 { keepalive 1000; server localhost:8020; } upstream grey_2 { keepalive ...

  2. Spring cloud架构中利用zuul网关实现灰度发布功能

    蓝绿发布.金丝雀发布(灰度发布).AB测试 首先,了解下这几种发布方式的基础概念. 目前常见的发布策略有蓝绿发布.金丝雀发布(灰度发布).AB测试这几种,在国内的开发者中,对这几个概念有独立的理解.蓝 ...

  3. springcloud灰度发布实现方案

    Nepxion Discovery是一款对Spring Cloud Discovery服务注册发现.Ribbon负载均衡.Feign和RestTemplate调用.Hystrix或者阿里巴巴Senti ...

  4. 基于ambassador实现K8S灰度发布

    为什么需要灰度发布 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式.在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对 ...

  5. Istio 太复杂?KubeSphere基于Ingress-Nginx实现灰度发布

    在 Bookinfo 微服务的灰度发布示例 中,KubeSphere 基于 Istio 对 Bookinfo 微服务示例应用实现了灰度发布.有用户表示自己的项目还没有上 Istio,要如何实现灰度发布 ...

  6. K8S基于ingress-nginx实现灰度发布

    之前介绍过使用ambassador实现灰度发布,今天介绍如何使用ingre-nginx实现. 介绍 Ingress-Nginx 是一个K8S ingress工具,支持配置 Ingress Annota ...

  7. 基于 Istio 与 Kubernetes 对应用进行灰度发布与 Tracing

    灰度发布,是指在黑与白之间,能够平滑过渡的一种发布方式.通俗来说,即让产品的迭代能够按照不同的灰度策略对新版本进行线上环境的测试,灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以对新版本进行测试 ...

  8. K8s 1.18.6版本基于 ingress-nginx 实现金丝雀发布(灰度发布)

    K8s 1.18.6版本基于 ingress-nginx 实现金丝雀发布(灰度发布) 环境 软件 版本 kubernetes v1.18.6 nginx-ingress-controller 0.32 ...

  9. Knativa 基于流量的灰度发布和自动弹性实践

    作者 | 李鹏(元毅) 来源 | Serverless 公众号 一.Knative Knative 提供了基于流量的自动扩缩容能力,可以根据应用的请求量,在高峰时自动扩容实例数:当请求量减少以后,自动 ...

随机推荐

  1. 原生js获取元素的各种位置(大全)

    加给元素: offsetLeft (距离定位父级的距离) offsetTop (距离定位父级的距离) offsetWidth (可视宽度) offsetHeight (可视高度) clientLeft ...

  2. poj2391 最大流+拆点+二分答案+Floyd

    Ombrophobic Bovines Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 19553   Accepted: 4 ...

  3. GYM100962A ABBA

    题目链接:https://vjudge.net/problem/Gym-100962A 题目大意: 给出一个 \(h \times w\) 的目标矩阵.定义一种 \(h \times w\) 的矩阵, ...

  4. Poj2109 (1) k^n = p.

    看到1<=p<10101 ,就去想大数操作了,后来看了discuss原来double完全可以放. 类型          长度 (bit)           有效数字          ...

  5. Springboot 关于日期时间格式化处理方式总结

    项目中使用LocalDateTime系列作为DTO中时间的数据类型,但是SpringMVC收到参数后总报错,为了配置全局时间类型转换,尝试了如下处理方式. 注:本文基于Springboot2.x测试, ...

  6. SpringCloud(三)- OpenFeign简介及@FeignClient等注解的使用

    唯能极于情,故能极于剑 有问题或错误请及时联系小编或关注小编公众号 "CodeCow",小编一定及时回复和改正,期待和大家一起学习交流 此文由四部分组成(OpenFeign简介.@ ...

  7. 【JavaScript数据结构系列】03-队列Queue

    [JavaScript数据结构系列]03-队列Queue 码路工人 CoderMonkey 转载请注明作者与出处 1. 认识队列Queue结构 队列,跟我们的日常生活非常贴近,我们前面举例了食堂排队打 ...

  8. Android_存储访问框架SAF

    概念 存储访问框架---Storage Access Framework (SAF),这是在Android4.4(API level 19)之后引入的. 借助 SAF,用户可轻松在其所有首选文档存储提 ...

  9. Java IO(十) BufferedInputStream 和 BufferedOutputStream

    Java IO(十)BufferedInputStream 和 BufferedOutputStream 一.BufferedInputStream 和 BufferedOutputStream (一 ...

  10. pandas如何逐行需改DataFrame

    逐行修改DataFrame而不会报SettingwithCopyWarning警告的方法: df.iloc[行数,df.columns.get_loc(列名)]=new_value 参考:https: ...