SpringCloudAlibaba基于nacos+feign+SpringCloudGateway组合来实现灰度发布

前言:

        当今,随着web2.0移动互联网的兴起,用户量的暴涨,各类网站应用的、各种APP规模也实现跨越式增长,随之而来的是各种高并发,海量数据处理的头疼问题,此时的系统架构为了使用时代,也被迫推陈出新。从互联网早期到现在,系统架构大体经历了下面几个过程:

单体应用架构--------垂直应用架构--------分布式架构--------SOA架构--------微服务架构

由于工作原因,需要对微服务灰度发布方面进行技术的预研与验证,顺便整理并形成实际文章,以便有所帮助。微服务涉及到的关键组件的功能在本案例不多做叙述。

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

        灰度发布开始到结束期间的这一段时间,称为灰度期。灰度发布能及早获得用户的意见反馈,完善产品功能,提升产品质量,让用户参与产品测试,加强与用户互动,降低产品升级所影响的用户范围。

     注:相关代码已上传到资源里,可在本人主页资源内下载源码进行测试技术交流

  1. 技术选型

 

本次方案选择SpringCloudAlibaba技术架构,具体采用 的是nacos+feign+SpringCloudGateway组合来实现灰度发布。当然也可以考虑采用 Dubbo+zookeeper方式进行服务的治理,来实现分布式服务的灰度发布,这里不多做体现。

2. 具体方案:

2.1 微服相关系统访问流程图解

         下面基于 GateWay和 Nacos实现微服务架构灰度发布方案,首先对生产的服务和灰度环境的服务统一注册到 Nacos中,但是版本不同,比如生产环境版本为 1.0,灰度环境版本为 2.0,请求经过网关后,判断携带的用户是否为灰度用户,如果是将请求转发至 2.0的服务中,否则转发到 1.0的服务中,并且微服务之间的访问也能按照此规则进行,如果没有灰度环境,则默认选择正式环境。本方案技术代码与nacos安装说明已打包放在主页资源中,需要时可下载。

 

 2.2 具体技术实现方案流程图解

       

    

3. 源码文件:

所用工具: IDEA,mysql,nacos

 3.1 整体构造

       

          pom.xml引用内容可以从本人资源中下载查看

 3.2 网关服务

 具体代码可以从本人资源中下载

    

  网关 application.yml配置:

   

server:
  port: 10010
logging:
  level:
    com.ecpmisrv: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates:  # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
#          filters:
#          - AddRequestHeader=Truth,Itcast is-freaking awesome!
      default-filters:
        - AddRequestHeader=Truth,victory!
      globalcors:
        add-to-simple-url-handler-mapping: true #解决options 请求被拦截问题
        cors-configurations:
          '/[**]':
            allowedOrigins: #允许哪些网站跨域请求
              - "http://localhost:8090"
              - "http://www.baidu.com"
            allowedMethods:
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" #允许请求头中带的头信息
            allowedCredenties: true #允许带Cookie
            maxAge: 36000 #这次跨域请求有效期


 pom.xml引用依赖:

  

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>com.ecpmisrv.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
        </dependency>

        <!--客户端负载均衡loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

    </dependencies>
</project>

网关 全链路流量标记

 

package com.ecpmisrv.gateway;


import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

//@Order {-1}  // 也可以采用实现 Orderd 接口方式来实现
@Component
@Slf4j
public class AuthorizeFilter implements GlobalFilter, Ordered {


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 1. 获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        //获取请求头
        HttpHeaders headers = request.getHeaders();
        System.out.println("-网关服务------headers-----"+headers.toString());
        // 2.或者判断是否是灰度用户 根据参数判断
       //------------针对请求体中参数进行校验---------
            // 2.1 使用客户端上传的版本参数或者使用redis缓存参数统一判断
            // 2.2 如果针对指定用户,可以加载白名单,配置数据库链接进行数据查询,与客户端传递过来的用户信息比对,一致的则打上灰度标记
            // 2.3 如果不针对特定用户或测试人员无法满足生产测试要求,可以采用nacos权重机制,分流,等待生产用户验证
        MultiValueMap<String, String> params = request.getQueryParams();
        log.info("----params---"+params.toString());
        // 2. 获取参数中的 authorization 参数
        String auth = params.getFirst("grayUserFlag");
        System.out.println("-网关服务---获取请求参数,判断是否灰度用户:"+auth);
        // 3. 判断参数值是否等于 admin
        if("YES".equals(auth)){
            // 拦截并设置  灰度环境
            //将灰度标记放入请求头中,放到后续链路中判断是否继续走其他的灰度服务
            ServerHttpRequest tokenRequest = request.mutate()
                    //将灰度标记传递过去  param: version  value: 2.0
                    .header("version","2.0")
                    .build();
            ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
            System.out.println("网关服务build 将灰度标记放入请求头中:--"+build.toString());
            grayscale("2.0"); //设置本地 ThreadLocal
            return chain.filter(build);
        }else {
            // 放行 正常环境
            return chain.filter(exchange);
        }
//            // 否 拦截
//        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//        return exchange.getResponse().setComplete();

    }

    /**
     * 灰度流程
     */
    private void grayscale(String version) {

        if (StringUtils.isEmpty(version)) {
            return;
        }

        if ("2.0".equals(version)) {

            // 设置当前用户灰度的环境
            GrayscaleThreadLocalEnvironment.setCurrentEnvironment("2.0");
        }else{
            // 设置当前环境为正式环境
            GrayscaleThreadLocalEnvironment.setCurrentEnvironment("1.0");
        }

    }

    @Override
    public int getOrder() {
        return -1;
    }
}

负载均衡策略-全服务统一

package com.ecpmisrv.gateway.config;

import com.ecpmisrv.gateway.GrayscaleThreadLocalEnvironment;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.google.common.base.Optional;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
@Slf4j
public class GrayRule extends ZoneAvoidanceRule {
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }

    @Override
    public Server choose(Object key) {
        try {

            //从ThreadLocal中获取灰度标记
            String version = GrayscaleThreadLocalEnvironment.getCurrentEnvironment();
            System.out.println("网关服务 从ThreadLocal中获取灰度标记: "+version);
            //获取所有服务
            List<Server> serverList = this.getLoadBalancer().getAllServers();
            System.out.println("-网关服务--------serverList---------"+serverList.toString());
            //灰度发布的服务
            List<Server> grayServerList = new ArrayList<Server>();
            //正常的服务
            List<Server> normalServerList = new ArrayList<Server>();
            for(Server server : serverList) {
                NacosServer nacosServer = (NacosServer) server;
                //从nacos中获取元素剧进行匹配
                if(nacosServer.getMetadata().containsKey("version")
                        && nacosServer.getMetadata().get("version").equals("2.0")) {
                    grayServerList.add(server);
                } else {
                    normalServerList.add(server);
                }
            }
            System.out.println("-网关服务---grayServerList----"+grayServerList.toString());
            System.out.println("-网关服务---normalServerList----"+normalServerList.toString());
            //如果被标记为灰度发布,则调用灰度发布的服务
            if("2.0".equals(version)) {
                Server grayServer = originChoose(grayServerList,key);
                if(null == grayServer || StringUtils.isEmpty(grayServer)){
                   log.info("无灰度服务或灰度服务列表中没有可用的服务,为保证服务能够正常进行,则将正式环境服务返回");
                    grayServer = originChoose(normalServerList,key);
                }
                 return grayServer;
            } else {
                return originChoose(normalServerList,key);
            }
        } finally {
            //清除灰度标记
            GrayscaleThreadLocalEnvironment.setCurrentEnvironment("1.0");
        }
    }

    private Server originChoose(List<Server> noMetaServerList, Object key) {
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
        System.out.println("-网关服务--noMetaServerList: "+noMetaServerList.toString());
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }
    }
}

3.3 订单服务

 

bootstrap.yml配置

spring:
  application:
    name: orderservice
#  profiles:
#    active: dev  #环境空间
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos地址
      discovery:
        metadata:
          version: 2.0 # 指定 是否灰度版本
#      config:
#        file-extension: yaml #文件格式
#        namespace: c145eeab-fd60-408e-91c6-b94d2910422f

feign拦截器-灰度流量标记

package com.ecpmisrv.order.config;

import com.ecpmisrv.feign.config.reliance.GrayscaleThreadLocalEnvironment;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

@Component
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {

        HttpServletRequest httpServletRequest =getHttpServletRequest();

        Map<String, String> headers = getHeaders(httpServletRequest);

        log.info("服务端微服务之间httpServletRequesteign调用headers: "+headers.toString());
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            //② 设置请求头到新的Request中
            template.header(entry.getKey(), entry.getValue());
        }
    }
    //获取请求对象
    private HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        } catch (Exception e) {
            //log.info("REQ1001","请求信息不能为空");
            return null;
        }
    }
    /**
     * 获取原请求头
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        if (enumeration != null) {
            while (enumeration.hasMoreElements()) {
                String key = enumeration.nextElement();
                String value = request.getHeader(key);
                //将灰度标记的请求头透传给下个服务
                if (key.equals("version")&&"2.0".equals(value)){
                    //① 保存灰度发布的标记
                    GrayscaleThreadLocalEnvironment.setCurrentEnvironment("2.0");
                    map.put(key, value);
                }

            }
        }
        return map;
    }
}

3.4 用户服务

 

3.5 公共依赖

 建议下载资源后,导入验证

4.测试验证

  4.1 nacos验证与配置

     本地安装nacos,注意本验证方案设置nacos端口号为8847。默认是8848。可以配置

      本方案采用的是单个nacos,后期采用集群化管理,通过ngix发起调用,本方案中不多做验证

  设置元数据

        也可以通过各个服务代码中的yml文件配置来实现

        

4.2 测试客户端发起

正式环境

 http://localhost:10010/order/101

 测试效果

 代码验证效果-网关服务日志打印,客户端对服务端orderservice正式环境服务进行调用

 

代码验证效果-orderserice服务8079接口日志打印,8080接口无日志打印。

 同时orderservice服务又对userservice服务进行调用,获取正式环境服务

灰度环境

 http://localhost:10010/order/101?grayUserFlag=YES

 网关服务日志,验证请求的是orderservice灰度环境服务

Orderservice服务8080 日志打印,显示已经选取userservice的灰度环境服务 

 

5. 总结

  技术架构最终是以业务实现为目标,具体业务场景如何定义,如何做到架构最优解,能满足后期迭代升级,都需要各位码农同仁努力,有任何问题欢迎交流评论。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>