栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

基于springcloud gateway + nacos实现灰度发布(reactive版)

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

基于springcloud gateway + nacos实现灰度发布(reactive版)

什么是灰度发布?

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

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

springcloud gateway官方介绍

nacos官方介绍

实现的整体思路:

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

下边进入实战

正文

1、所使用的开发版本

    1.8
    
    Hoxton.SR3
    2.2.5.RELEASE
    2.2.1.RELEASE

2、pom.xml引入

   
 
     org.springframework.cloud
     spring-cloud-starter-gateway
 

 
     org.springframework.boot
     spring-boot-starter-webflux
 

 
     com.alibaba.cloud
     spring-cloud-starter-alibaba-nacos-discovery
     
  
      org.springframework.cloud
      spring-cloud-starter-netflix-ribbon
  
     
 


 
     org.springframework.cloud
     spring-cloud-loadbalancer
 

 
     org.apache.commons
     commons-lang3
 

    

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

3、编写权重路由

 public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(GrayLoadBalancer.class);
    private ObjectProvider serviceInstanceListSupplierProvider;
    private  String serviceId;




    public GrayLoadBalancer(ObjectProvider serviceInstanceListSupplierProvider, String serviceId) {
 this.serviceId = serviceId;
 this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }


    @Override
    public Mono> 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)list,headers));
 }

 return null;


    }



    private Response getInstanceResponse(List instances,HttpHeaders headers) {
 if (instances.isEmpty()) {
     return getServiceInstanceEmptyResponse();
 } else {
     return getServiceInstanceResponseWithWeight(instances);
 }
    }

    
    private Response getServiceInstanceResponseByVersion(List instances, HttpHeaders headers) {
 String versionNo = headers.getFirst("version");
 System.out.println(versionNo);
 Map versionMap = new HashMap<>();
 versionMap.put("version",versionNo);
 final Set> attributes =
  Collections.unmodifiableSet(versionMap.entrySet());
 ServiceInstance serviceInstance = null;
 for (ServiceInstance instance : instances) {
     Map metadata = instance.getmetadata();
     if(metadata.entrySet().containsAll(attributes)){
  serviceInstance = instance;
  break;
     }
 }

 if(ObjectUtils.isEmpty(serviceInstance)){
     return getServiceInstanceEmptyResponse();
 }
 return new DefaultResponse(serviceInstance);
    }

    
    private Response getServiceInstanceResponseWithWeight(List instances) {
 Map weightMap = new HashMap<>();
 for (ServiceInstance instance : instances) {
     Map 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 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 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 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> 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 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

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号