springcloud结合nacos实现灰度发版方案


最近和组内伙伴一起分享讨论了灰度方案,先将内容整理出来, 具体如下:

灰度方案设计及demo实现

  • 架构设计
  • 组件设计及原理
  • 关键代码实现
  • 项目资料

    一. 架构设计

    微服务架构图.png

    原理

1:微服务系统在启动时将自己注册到服务注册中心,同时对外发布 Http 接口供其它系统调用(一般都是基于Spring MVC)
2:服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对 加了该注解的接口生成动态代理,服务消费者会针对 Feign 生成的动态代理去调用方法时,在底层会生成Http协议格式的请求,类似 /stock/deduct?productId=100
3:Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡 并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成类似调用http接口地址 http://192.168.0.60:9000/stock/deduct?productId=100 最后基于HTTPClient调用请求

基于微服务架构的原理,来设计灰度方案

基于微服务架构的原理,来设计灰度方案.png

概要流程:

1.全局配置灰度是否启用–在nacos中配置, 动态更新
2.配置灰度规则, version=2.0 userId=”1234567” productId=”010-1234567”
3.设置灰度服务器, 哪些服务器是灰度服务器。 为其打标签
4.启动所有服务, 服务在nacos上进行注册
5.客户端发起请求, 带着header参数
6.zuul进行过滤,判断是否符合灰度条件, 如果符合,打上灰度标签
7.通过feign将灰度标签进行透传
8.通过ribbon选择跳转的服务器, 可以指定负载均衡策略
9.下一个服务器继续跳转,带上feign的灰度标签,继续请求。

以上是这个灰度方案实现的整体逻辑和思路

二. 组件设计及原理

2.1 灰度的目标

不同的流量过来, 根据元数据匹配, 走不同的微服务
灰度模型.png
当流量请求过来以后, 根据其匹配的灰度规则的不同, 走的服务有所不同, 可以将其分为三种类型.

  • 不匹配任何灰度规则, 则走无灰度服务
  • 匹配灰度规则, 则走对应的灰度服务
  • 同时匹配多个灰度规则, 选择灰度服务

    2.2 管理后台–设置并管理灰度规则

    全局灰度标签设置在nacos中, nacos配置的灰度标签的开闭, 可实时自动更新同步.
    灰度管理后台, 管理后台主要有两大块内容.
      1) 配置灰度规则
        1. 根据需要设置灰度规则, 比如: 城市, 用户id, 订单id, 版本号, 学科等
      2) 设置灰度服务器
        1. 调用nacos接口, 获取所有微服务ip+port
        2. 为灰度服务器打灰度标签
        3. 做同步策略, 当灰度服务标签内容有变化, 通知网关, 做相应更新
    灰度规则设置.png

    2.3. 网关设置–拦截请求, 为其打灰度标签

    网关其实就是各种各样的过滤器, 常用的过滤器类型有:pre:前置过滤器, routing: 路由过滤器, post过滤器, error过滤器
    这里我们定义一个前置过滤器, 过滤所有 过来的请求, 判断其是否匹配灰度规则

执行步骤:

1:初始化灰度规则, 我们首先判断nacos中灰度规则是否启用, 启用则去灰度管理服务器获取有效的灰度规则
2:判断请求头是否和某一灰度规则匹配, 如果匹配, 则将请求header添加到请求上下文, 后续feign进行透传. 同时添加到ribbon请求上下文, 做服务选择.

gateway灰度规则.png

2.4. ribbon设置 – 根据灰度规则, 选择灰度服务器

ribbon是客户端负载均衡, 通过对ribbon上下文中的灰度标签和微服务列表中灰度标签的比较, 来选择一台服务器, 作为目标跳转服务器
ribbon-loadbalance-rule.png

2.5 自定义Feign拦截器, 实现参数(灰度标签)的透传

feign的实质是拦截器, feign将拦截所有的请求跳转, 主要作用是用来做header参数透传, 保证服务间的调用也可以正确选择灰度服务器.
custom-feign-intercept.png

三. demo设计规划及实现

3.1. 微服务规划

微服务样例列表.png

3.2 gateway关键代码实现

/**
     * 过滤器执行的内容
     * @return
     */
    @Override
    public Object run() {
        // 第一步: 初始化灰度规则
        if (!initGray) {
            //初始化灰度规则
            getGrayRules();
        }

        // 第二步: 获取请求头(包括请求的来源url和method)
        Map<String, String> headerMap = getHeadersInfo();
        log.info("headerMap:{},grayRules:{}", headerMap, grayRules);

        // 删除之前的路由到灰度的标记
       /* if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) {
            RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG);
        }*/
        //灰度开关关闭 -- 无需走灰度, 执行正常的ribbon负载均衡转发策略
        if (grayEnable == 0) {
            log.info("灰度开关已关闭");
            return null;
        }
        if (!grayRules.isEmpty()) {

            for (Map<String, String> grayRuleMap : grayRules) {
                try {
                    // 获取本次灰度的标签,标签的内容是灰度的规则内容
                    String grayTag = grayRuleMap.get(GrayConstant.GRAY_TAG);

                    // 第三步: 过滤有效的灰度标签
                    Map<String, String> resultGrayRuleMap = new HashMap<>();
                    //去掉值为空的灰度规则
                    grayRuleMap.forEach((K, V) -> {
                        if (StringUtils.isNotBlank(V)) {
                            resultGrayRuleMap.put(K, V);
                        }
                    });
                    resultGrayRuleMap.remove(GrayConstant.GRAY_TAG);


                    //将灰度标签(规则)小写化
                    Map<String, String> lowerGrayRuleMap = transformUpperCase(resultGrayRuleMap);

                    // 第四步: 判断请求头是否匹配灰度规则
                    if (headerMap.entrySet().containsAll(resultGrayRuleMap.entrySet()) || headerMap.entrySet().containsAll(lowerGrayRuleMap.entrySet())) {
                        // 这是网关通讯使用的全局对象RequestContext
                        RequestContext requestContext = RequestContext.getCurrentContext();
                        // 把灰度规则添加到网关请求头, 后面的请求都可以使用该参数
                        requestContext.addZuulRequestHeader(GrayConstant.GRAY_HEADER, grayTag);
                        // 将灰度规则添加到ribbon的上下文
                        RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, grayTag);
                        log.info("添加灰度tag成功:lowerGrayRuleMap:{},grayTag:{}", lowerGrayRuleMap, grayTag);
                    }
                } catch (Exception e) {
                    log.error("灰度匹配失败", e);
                }
            }
        }
        return null;
    }

这里的逻辑

首先: 获取灰度规则标签. 什么时候获取呢? 第一次请求过来的时候, 去请求灰度标签. 放到全局的map集合中. 后面, 直接拿来就用

第二: 获取请求过来的header, 和灰度规则进行匹配, 如果匹配上了, 那么打灰度标签, 将其灰度请求头添加到请求上下文, 同时添加到ribbon请求的上下文中

接下来, 走feign实现header透传

3.3 feign关键代码实现

@Override
    public void apply(RequestTemplate requestTemplate) {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        //处理特殊情况
        if (null == ra) {
            return;
        }
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();

        //处理特殊情况
        if (null == request) {
            return;
        }
        log.info("[feign拦截器] ribbon上下文属性:{}", JSON.toJSONString(RibbonFilterContextHolder.getCurrentContext().getAttributes()));
        if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) {
            RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG);
        }
        if (StringUtils.isNotBlank(request.getHeader(GrayConstant.GRAY_HEADER))) {
            log.info("灰度feign收到header:{}", request.getHeader(GrayConstant.GRAY_HEADER));
            RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, request.getHeader(GrayConstant.GRAY_HEADER));
            requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER));
        }
    }

其实feign的主要作用就是透传, 为什么要透传了呢? 微服务之间的请求, 不只是是首次定向的服务需要进行灰度, 那么后面服务内部相互调用也可能要走灰度, 那么最初请求的请求头就很重要了. 要一直传递下去.

而requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER));就可以实现参数在整个请求进行透传.

请求的参数带好了, 下面就要进行服务选择了, 有n台服务器, 到底要选择哪台服务器呢? 就是ribbon的负载均衡选择了

3.4 ribbon关键代码实现

/**
     * 实现父类的负载均衡规则
     *
     * @param key
     * @return
     */
    @Override
    public Server choose(Object key) {
        //return choose(getLoadBalancer(), key);
        try {
            // 调用父类方法, 获取当前的负载均衡器
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();

            //获取当前的服务名
            String serviceName = loadBalancer.getName();
            log.info("[ribbon负载均衡策略] 当前服务名: {}", serviceName);
            //获取服务发现客户端
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();

            // 获取指定的服务实例列表
            List<Instance> allInstances = namingService.getAllInstances(serviceName);
            log.info("[ribbon负载均衡策略] 可用的服务实例: {}", allInstances);

            if (allInstances == null || allInstances.size() == 0) {
                log.warn("没有可用的服务器");
                return null;
            }

            RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
            log.info("MetadataBalancerRule RibbonFilterContext:{}", context.getAttributes());
            Set<Map.Entry<String, String>> ribbonAttributes = context.getAttributes().entrySet();

            /**
             * 服务分为三种类型
             * 1. 设置为灰度的服务   ---   灰度服务
             * 2. 先设置了灰度, 后取消了灰度的服务   ---   去灰服务
             * 3. 普通服务-非灰服务
             */
            // 可供选择的灰度服务
            List<Instance> grayInstances = new ArrayList<>();

            // 非灰服务
            List<Instance> noneGrayInstances = new ArrayList<>();

            Instance toBeChooseInstance;

            if (!context.getAttributes().isEmpty()) {
                for (Instance instance : allInstances) {
                    Map<String, String> metadata = instance.getMetadata();
                    if (metadata.entrySet().containsAll(ribbonAttributes)) {
                        log.info("进行灰度匹配,已匹配灰度服务:{},灰度tag为:{}", instance, context.getAttributes().get(GrayConstant.GRAY_TAG));
                        grayInstances.add(instance);
                    } else if (!StringUtils.isBlank(metadata.get(GrayConstant.GRAY_TAG))) {
                        // 非灰度服务
                        noneGrayInstances.add(instance);
                    }
                }
            }

            log.info("[ribbon负载均衡策略] 灰度服务: {}, 非灰服务:{}", grayInstances, noneGrayInstances);

            // 如果灰度服务不为空, 则走灰度服务
            if (grayInstances != null && grayInstances.size() &gt; 0) {
                // 走灰度服务 -- 从本集群中按照权重随机选择一个服务实例
                toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(grayInstances);
                log.info("[ribbon负载均衡策略] 灰度规则匹配成功, 匹配的灰度服务是: {}", toBeChooseInstance);
                return new NacosServer(toBeChooseInstance);
            }

            // 灰度服务为空, 走非断灰的服务
            if (noneGrayInstances != null && noneGrayInstances.size() &gt; 0) {
                // 走非灰服务 -- 从本集群中按照权重随机选择一个服务实例
                toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(noneGrayInstances);
                log.info("[ribbon负载均衡策略] 不走灰度, 匹配的非灰度服务是: {}", toBeChooseInstance);
                return new NacosServer(toBeChooseInstance);
            } else {
                log.info("未找到可匹配服务,实际服务:{}", allInstances);
                toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(allInstances);
                log.info("[ribbon负载均衡策略] 未找到可匹配服务, 随机选择一个: {}", toBeChooseInstance);
                return new NacosServer(toBeChooseInstance);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

以上是梳理了网关的主要逻辑思想和关键代码

四. 项目资料

关于以上内容相关的源码,有需要的伙伴可以赞助小张哥喝一杯咖啡,然后在赞赏备注中留下自己的email信息,我会转发code。(code是可以直接运行通过的哦,编译过程中有什么问题可以直接在文章下方给我留言,我会实时通过邮件的方式进行查收的~~)
小张哥的赞赏码


文章作者: 小张哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小张哥 !
评论
 上一篇
spring5源码 - IOC加载过程 Bean的生命周期 spring5源码 - IOC加载过程 Bean的生命周期
目录 spring整体脉络 描述BeanFactory BeanFactory和ApplicationContext的区别 简述SpringIoC的加载过程 简述Bean的生命周期 Spring中有哪些扩展接口及调用机制 一. sp
下一篇 
微服务网关zuul限流、熔断、路由规则详解 微服务网关zuul限流、熔断、路由规则详解
简介最近在研究灰度方案。 灰度主要涉及两个方面,一个是网关,一个是配置。 这次主要研究了网关及其原理,我们现在使用项目每天都有很多流量进入。 每一个流量基本都要经过网关。所以弄清楚网关也可以帮我们更好的理解微服务请求的转发,对于后面研究灰度
2021-10-13
  目录