3

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

 2 years ago
source link: https://xiaomozhang.github.io/2021/10/14/springcloud-nacos-gray-plan-demo/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

灰度方案设计及demo实现

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

    一. 架构设计

    微服务架构图.png
    微服务架构图.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
基于微服务架构的原理,来设计灰度方案.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
灰度模型.png
当流量请求过来以后, 根据其匹配的灰度规则的不同, 走的服务有所不同, 可以将其分为三种类型.
  • 不匹配任何灰度规则, 则走无灰度服务
  • 匹配灰度规则, 则走对应的灰度服务
  • 同时匹配多个灰度规则, 选择灰度服务

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

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

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

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

执行步骤:

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

gateway灰度规则.png
gateway灰度规则.png

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

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

ribbon-loadbalance-rule.png
ribbon-loadbalance-rule.png

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

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

custom-feign-intercept.png
custom-feign-intercept.png

三. demo设计规划及实现

3.1. 微服务规划

微服务样例列表.png
微服务样例列表.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() > 0) {
                // 走灰度服务 -- 从本集群中按照权重随机选择一个服务实例
                toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(grayInstances);
                log.info("[ribbon负载均衡策略] 灰度规则匹配成功, 匹配的灰度服务是: {}", toBeChooseInstance);
                return new NacosServer(toBeChooseInstance);
            }

            // 灰度服务为空, 走非断灰的服务
            if (noneGrayInstances != null && noneGrayInstances.size() > 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是可以直接运行通过的哦,编译过程中有什么问题可以直接在文章下方给我留言,我会实时通过邮件的方式进行查收的~~)

小张哥的赞赏码
小张哥的赞赏码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK