专栏首页Linyb极客之路基于springcloud gateway + nacos实现灰度发布(reactive版)

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

什么是灰度发布?

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

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

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/ https://nacos.io/zh-cn/docs/quick-start.html

实现的整体思路:

  • 编写带权重的灰度路由
  • 编写自定义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

本文分享自微信公众号 - Linyb极客之路(gh_c420b2cf6b47),作者:linyb极客之路

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

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

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

    lyb-geek
  • 干货分享:Postman使用小技巧

    从该点的下拉框可以查看到已经设置的变量!如果之前未设置过变量的,可以点击上图中红色框后边的齿轮按钮进入到环境变量添加页面中,添加页面如下图:

    lyb-geek
  • springboot实战之mysql分库分表

    把存于一个库的数据分散到多个库中,把存于一个表的数据分散到多个表中。如果说读写分离是为了分散数据库读写操作压力,分库分表就是为了分散存储压力

    lyb-geek
  • Did you install mysqlclient?

    小贝壳
  • jenkins离线安装问题解决记录

    出现这个问题的原因是因为在下载插件之前会检查网络连接,而检查网络回去读/updates/default.json这个文件,而这个文件指定的访问检查网址为www....

    dogfei
  • 【干货分享】揭秘腾迅未来到底想要干吗?

    腾讯开放平台总监陈鹏在万科营销系统年会上发表演讲,主要阐述开放互联网的3大块:平台、服务能力和变现渠道。这是一篇干货很多的演讲,腾讯现在的理念是什么,腾讯靠什么...

    人称T客
  • Ubuntu下pip工具安装遇到的问题

    Ubuntu 14.04.3环境下进行Python开发的时候遇到如下问题。 安装pip的时候,首先需要安装setuptools,使用命令sudo apt-g...

    卡尔曼和玻尔兹曼谁曼
  • Ionic3学习笔记(七)Storage

    Storage可以很容易的存储键值对和JSON对象。Storage在底层使用多种存储引擎,根据运行平台选择最佳的存储方式。 当运行在Native模式时,Stor...

    Theo Tsao
  • 高屋建瓴地规划自己的运维体系

    说简单,倒也简单:运维工作就是支持生产运行,是成本中心,一般不直接产生利润。目的就是运行保生产设备软硬件正常运行,让内外部用户满意度。

    博文视点Broadview
  • 病毒分析快速入门(二)--实战QuasarRAT

    样本可从app.any.run获取,使用邮箱免费注册后,便可以下载该沙箱的公开样本

    Gcow安全团队

扫码关注云+社区

领取腾讯云代金券