基于spring cloud 的灰度发布实践_基于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、所使用的开发版本

    1.8Hoxton.SR32.2.5.RELEASE2.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);        }    }    /**     * 根据版本进行分发     * @param instances     * @param headers     * @return     */    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);    }    /**     *     * 根据在nacos中配置的权重值,进行分发     * @param instances     *     * @return     */    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管理

@Configurationpublic 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配置灰度发布的服务版本以及权重值

d9552bb64972e8a26f43737c60d005a8.png

123.jpg

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值