SpringCloud之Gateway灰度发布讲解

1 灰度发布

1.1 简介

1.1.1 概念

灰度发布, 也叫金丝雀发布。是指在黑与白之间,能够平滑过渡的一种发布方式。AB test 就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。

具体到服务器上,实际操作中还可以做更多控制,譬如说,给最初更新的10台服务器设置较低的权重、控制发送给这10台服务器的请求数,然后逐渐提高权重、增加请求数。一种平滑过渡的思路, 这个控制叫做流量切分

1.1.2 组件版本

使用组件说明:

  • spring-boot: 2.3.12.RELEASE
  • spring-cloud-dependencies: Hoxton.SR12
  • spring-cloud-alibaba-dependencies: 2.2.9.RELEASE

核心组件说明:

  • 注册中心: Nacos
  • 网关: SpringCloudGateway
  • 负载均衡器: Ribbon (使用SpringCloudLoadBalancer实现也是类似的)
  • 服务间RPC调用: OpenFeign

1.2 代码设计

1.2.1 设计图示

要实现Spring Cloud项目灰度发布技术方案有很多,重点在于服务发现,怎么将灰度流量只请求到灰度服务,这里会使用Nacos作为注册中心和配置中心,核心就是利用NacosMetadata设置一个version值,在调用下游服务是通过version值来区分要调用那个版本。
在这里插入图片描述

1.2.2 代码设计结构

这个是demo项目,结构都按最简单的来。

spring-cloud-gray-example // 父工程
   kerwin-common // 项目公共模块
   kerwin-gateway // 微服务网关
   kerwin-order // 订单模块
      order-app // 订单业务服务
   kerwin-starter // 自定义springboot starter模块
      spring-cloud-starter-kerwin-gray // 灰度发布starter包 (核心代码都在这里)
   kerwin-user // 用户模块
      user-app // 用户业务服务
      user-client // 用户client(Feign和DTO)

核心包spring-cloud-starter-kerwin-gray结构介绍
在这里插入图片描述

1.3 gateway和ribbon操作

1.3.1 存取请求灰度标记Holder

使用ThreadLocal记录每个请求线程的灰度标记,会在前置过滤器中将标记设置到ThreadLocal中。

注意ThreadLocal 的作用范围仅限于当前线程,且在当前 JVM 内有效。在微服务环境下,ThreadLocal 的数据无法跨服务传递,也无法跨线程传递

public class GrayFlagRequestHolder {
    /**
     * 标记是否使用灰度版本
     * 具体描述请查看 {@link com.kerwin.gray.enums.GrayStatusEnum}
     */
    private static final ThreadLocal<GrayStatusEnum> grayFlag = new ThreadLocal<>();
    public static void setGrayTag(final GrayStatusEnum tag) {
        grayFlag.set(tag);
    }
    public static GrayStatusEnum getGrayTag() {
        return grayFlag.get();
    }
    public static void remove() {
        grayFlag.remove();
    }
}

1.3.2 前置过滤器

在前置过滤器中会对请求是否要使用灰度版本进行判断,并且会将灰度状态枚举GrayStatusEnum设置到GrayRequestContextHolder中存储这一个请求的灰度状态枚举,在负载均衡器中会取出灰度状态枚举判断要调用那个版本的服务,同时这里还实现了 Ordered 接口会对网关的过滤器进行的排序,这里我们将这个过滤器的排序设置为Ordered.HIGHEST_PRECEDENCE int的最小值,保证这个过滤器最先执行

public class GrayGatewayBeginFilter implements GlobalFilter, Ordered {
    @Autowired
    private GrayGatewayProperties grayGatewayProperties;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        GrayStatusEnum grayStatusEnum = GrayStatusEnum.ALL;
        // 当灰度开关打开时才进行请求头判断
        if (grayGatewayProperties.getEnabled()) {
            grayStatusEnum = GrayStatusEnum.PROD;
            // 判断是否需要调用灰度版本
            if (checkGray(exchange.getRequest())) {
                grayStatusEnum = GrayStatusEnum.GRAY;
            }
        }
        GrayFlagRequestHolder.setGrayTag(grayStatusEnum);
        ServerHttpRequest newRequest = exchange.getRequest().mutate()
                .header(GrayConstant.GRAY_HEADER, grayStatusEnum.getVal())
                .build();
        ServerWebExchange newExchange = exchange.mutate()
                .request(newRequest)
                .build();
        return chain.filter(newExchange);
    }

    /**
     * 校验是否使用灰度版本
     */
    private boolean checkGray(ServerHttpRequest request) {
        if (checkGrayHeadKey(request) || checkGrayIPList(request) || checkGrayCiryList(request) || checkGrayUserNoList(request)) {
            return true;
        }
        return false;
    }

    /**
     * 校验自定义灰度版本请求头判断是否需要调用灰度版本
     */
    private boolean checkGrayHeadKey(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        if (headers.containsKey(grayGatewayProperties.getGrayHeadKey())) {
            List<String> grayValues = headers.get(grayGatewayProperties.getGrayHeadKey());
            if (!Objects.isNull(grayValues)
                    && grayValues.size() > 0
                    && grayGatewayProperties.getGrayHeadValue().equals(grayValues.get(0))) {
                return true;
            }
        }
        return false;
    }

    /**
     * 校验自定义灰度版本IP数组判断是否需要调用灰度版本
     */
    private boolean checkGrayIPList(ServerHttpRequest request) {
        List<String> grayIPList = grayGatewayProperties.getGrayIPList();
        if (CollectionUtils.isEmpty(grayIPList)) {
            return false;
        }
        String realIP = request.getHeaders().getFirst("X-Real-IP");
        if (realIP == null || realIP.isEmpty()) {
            realIP = request.getRemoteAddress().getAddress().getHostAddress();
        }
        if (realIP != null && CollectionUtils.contains(grayIPList.iterator(), realIP)) {
            return true;
        }
        return false;
    }

    /**
     * 校验自定义灰度版本城市数组判断是否需要调用灰度版本
     */
    private boolean checkGrayCiryList(ServerHttpRequest request) {
        List<String> grayCityList = grayGatewayProperties.getGrayCityList();
        if (CollectionUtils.isEmpty(grayCityList)) {
            return false;
        }
        String realIP = request.getHeaders().getFirst("X-Real-IP");
        if (realIP == null || realIP.isEmpty()) {
            realIP = request.getRemoteAddress().getAddress().getHostAddress();
        }
        // 通过IP获取当前城市名称
        // 想要实现的可以使用ip2region.xdb,这里写死cityName = "本地"
        String cityName = "本地";
        if (cityName != null && CollectionUtils.contains(grayCityList.iterator(), cityName)) {
            return true;
        }
        return false;
    }

    /**
     * 校验自定义灰度版本用户编号数组(我们系统不会在网关获取用户编号这种方法如果需要可以自己实现一下)
     */
    private boolean checkGrayUserNoList(ServerHttpRequest request) {
        List<String> grayUserNoList = grayGatewayProperties.getGrayUserNoList();
        if (CollectionUtils.isEmpty(grayUserNoList)) {
            return false;
        }
        return false;
    }

    @Override
    public int getOrder() {
        // 设置过滤器的执行顺序,值越小越先执行
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

1.3.3 后置过滤器

后置过滤器是为了在调用完下游业务服务后在响应之前将 GrayFlagRequestHolder 中的 ThreadLocal 清除避免造成内存泄漏。

public class GrayGatewayAfterFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 请求执行完必须要remore当前线程的ThreadLocal
        GrayFlagRequestHolder.remove();
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 设置过滤器的执行顺序,值越小越先执行
        return Ordered.LOWEST_PRECEDENCE;
    }
}

1.3.4 全局异常处理器

全局异常处理器是为了处理异常情况下将 GrayFlagRequestHolder 中的 ThreadLocal 清除避免造成内存泄漏,如果在调用下游业务服务时出现了异常就无法进入后置过滤器。

public class GrayGatewayExceptionHandler implements WebExceptionHandler, Ordered {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 请求执行完必须要remore当前线程的ThreadLocal
        GrayFlagRequestHolder.remove();
        ServerHttpResponse response = exchange.getResponse();
        if (ex instanceof ResponseStatusException) {
            // 处理 ResponseStatusException 异常
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            response.setStatusCode(responseStatusException.getStatus());
            // 可以根据需要设置响应头等
            return response.setComplete();
        } else {
            // 处理其他异常
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            // 可以根据需要设置响应头等
            return response.setComplete();
        }
    }

    @Override
    public int getOrder() {
        // 设置过滤器的执行顺序,值越小越先执行
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

1.3.5 自定义Ribbon负载均衡路由

灰度Ribbon负载均衡路由抽象类:这里提供了两个获取服务列表的方法,会对 GrayFlagRequestHolder 中存储的当前线程灰度状态枚举进行判断,如果枚举值为 GrayStatusEnum.ALL 则响应全部服务列表不区分版本,如果枚举值为GrayStatusEnum.PROD 则返回生产版本的服务列表,如果枚举值为 GrayStatusEnum.GRAY 则返回灰度版本的服务列表,版本号会在GrayVersionProperties 中配置,通过服务列表中在Nacosmetadata中设置的version和GrayVersionProperties的版本号进行匹配出对应版本的服务列表。

import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ILoadBalancer;

public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
    @Autowired
    private GrayVersionProperties grayVersionProperties;

    @Value("${spring.cloud.nacos.discovery.metadata.version}")
    private String metaVersion;

    /**
     * 只有已启动且可访问的服务器,并对灰度标识进行判断
     */
    public List<Server> getReachableServers() {
        ILoadBalancer lb = getLoadBalancer();
        if (lb == null) {
            return new ArrayList<>();
        }
        List<Server> reachableServers = lb.getReachableServers();

        return getGrayServers(reachableServers);
    }

    /**
     * 所有已知的服务器,可访问和不可访问,并对灰度标识进行判断
     */
    public List<Server> getAllServers() {
        ILoadBalancer lb = getLoadBalancer();
        if (lb == null) {
            return new ArrayList<>();
        }
        List<Server> allServers = lb.getAllServers();
        return getGrayServers(allServers);
    }

    /**
     * 获取灰度版本服务列表
     */
    protected List<Server> getGrayServers(List<Server> servers) {
        List<Server> result = new ArrayList<>();
        if (servers == null) {
            return result;
        }
        String currentVersion = metaVersion;
        GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
        if (grayStatusEnum != null) {
            switch (grayStatusEnum) {
                case ALL:
                    return servers;
                case PROD:
                    currentVersion = grayVersionProperties.getProdVersion();
                    break;
                case GRAY:
                    currentVersion = grayVersionProperties.getGrayVersion();
                    break;
            }
        }

        for (Server server : servers) {
            NacosServer nacosServer = (NacosServer) server;
            Map<String, String> metadata = nacosServer.getMetadata();
            String version = metadata.get("version");
            // 判断服务metadata下的version是否于设置的请求版本一致
            if (version != null && version.equals(currentVersion)) {
                result.add(server);
            }
        }
        return result;
    }
}

自定义轮询算法实现GrayRoundRobinRule: 这里是直接拷贝了Ribbon的轮询算法,将里面获取服务列表的方法换成了自定义AbstractGrayLoadBalancerRule 中的方法,其它算法也可以通过类似的方式实现。
在这里插入图片描述

1.4 业务服务操作

1.4.1 自定义SpringMVC请求拦截器

自定义SpringMVC请求拦截器获取上游服务的灰度请求头,如果获取到则设置到 GrayFlagRequestHolder 中,之后如果有后续的 RPC 调用同样的将灰度标记传递下去。

@SuppressWarnings("all")
public class GrayMvcHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String grayTag = request.getHeader(GrayConstant.GRAY_HEADER);
        // 如果HttpHeader中灰度标记存在,则将灰度标记放到holder中,如果需要就传递下去
        if (grayTag!= null) {
            GrayFlagRequestHolder.setGrayTag(GrayStatusEnum.getByVal(grayTag));
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        GrayFlagRequestHolder.remove();
    }
}

1.4.2 自定义OpenFeign请求拦截器

自定义OpenFeign请求拦截器,取出自定义SpringMVC请求拦截器中设置到GrayFlagRequestHolder中的灰度标识,并且放到调用下游服务的请求头中,将灰度标记传递下去。

public class GrayFeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 如果灰度标记存在,将灰度标记通过HttpHeader传递下去
        GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
        if (grayStatusEnum != null ) {
            template.header(GrayConstant.GRAY_HEADER, Collections.singleton(grayStatusEnum.getVal()));
        }
    }
}

1.5 配置信息

1.5.1 配置类

这里会定义一些基础参数,比如是否开启灰度还有什么请求需要使用灰度版本等,为后续业务做准备。
调用业务服务时设置的灰度统一请求头

public interface GrayConstant {
    /**
     * 灰度统一请求头
     */
    String GRAY_HEADER="gray";
}

灰度版本状态枚举

public enum GrayStatusEnum {
    ALL("ALL","可以调用全部版本的服务"),
    PROD("PROD","只能调用生产版本的服务"),
    GRAY("GRAY","只能调用灰度版本的服务");
    GrayStatusEnum(String val, String desc) {
        this.val = val;
        this.desc = desc;
    }
    private String val;
    private String desc;
    public String getVal() {
        return val;
    }
    public static GrayStatusEnum getByVal(String val){
        if(val == null){
            return null;
        }
        for (GrayStatusEnum value : values()) {
            if(value.val.equals(val)){
                return value;
            }
        }
        return null;
    }
}

网关灰度配置信息类

@Data
@Configuration
@RefreshScope
@ConfigurationProperties("kerwin.tool.gray.gateway")
public class GrayGatewayProperties {

    /**
     * 灰度开关(如果开启灰度开关则进行灰度逻辑处理,如果关闭则走正常处理逻辑)
     * PS:一般在灰度发布测试完成以后会将线上版本都切换成灰度版本完成全部升级,这时候应该关闭灰度逻辑判断
     */
    private Boolean enabled = false;

    /**
     * 自定义灰度版本请求头 (通过grayHeadValue来匹配请求头中的值如果一致就去调用灰度版本,用于公司测试)
     */
    private String grayHeadKey="gray";

    /**
     * 自定义灰度版本请求头匹配值
     */
    private String grayHeadValue="gray-996";

    /**
     * 使用灰度版本IP数组
     */
    private List<String> grayIPList = new ArrayList<>();

    /**
     * 使用灰度版本城市数组
     */
    private List<String> grayCityList = new ArrayList<>();

    /**
     * 使用灰度版本用户编号数组(我们系统不会在网关获取用户编号这种方法如果需要可以自己实现一下)
     */
    private List<String> grayUserNoList = new ArrayList<>();
}

全局版本配置信息类

@Data
@Configuration
@RefreshScope
@ConfigurationProperties("kerwin.tool.gray.version")
public class GrayVersionProperties {
    /**
     * 当前线上版本号
     */
    private String prodVersion;

    /**
     * 灰度版本号
     */
    private String grayVersion;
}

全局自动配置类

@Configuration
// 可以通过@ConditionalOnProperty设置是否开启灰度自动配置 默认是不加载的
@ConditionalOnProperty(value = "kerwin.tool.gray.load",havingValue = "true")
@EnableConfigurationProperties(GrayVersionProperties.class)
public class GrayAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(value = GlobalFilter.class)
    @EnableConfigurationProperties(GrayGatewayProperties.class)
    static class GrayGatewayFilterAutoConfiguration {
        @Bean
        public GrayGatewayBeginFilter grayGatewayBeginFilter() {
            return new GrayGatewayBeginFilter();
        }
        @Bean
        public GrayGatewayAfterFilter grayGatewayAfterFilter() {
            return new GrayGatewayAfterFilter();
        }
        @Bean
        public GrayGatewayExceptionHandler grayGatewayExceptionHandler(){
            return new GrayGatewayExceptionHandler();
        }
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(value = WebMvcConfigurer.class)
    static class GrayWebMvcAutoConfiguration {
        /**
         * Spring MVC 请求拦截器
         * @return WebMvcConfigurer
         */
        @Bean
        public WebMvcConfigurer webMvcConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addInterceptor(new GrayMvcHandlerInterceptor());
                }
            };
        }
    }
    @Configuration
    @ConditionalOnClass(value = RequestInterceptor.class)
    static class GrayFeignInterceptorAutoConfiguration {
        /**
         * Feign拦截器
         * @return GrayFeignRequestInterceptor
         */
        @Bean
        public GrayFeignRequestInterceptor grayFeignRequestInterceptor() {
            return new GrayFeignRequestInterceptor();
        }
    }
}

1.5.2 配置文件

配置Nacos全局配置文件(common-config.yaml)

kerwin:
  tool:
    gray:
      # 配置是否加载灰度自动配置类,如果不配置那么默认不加载
      load: true
      # 配置生产版本和灰度版本号
      version:
        prodVersion: V1
        grayVersion: V2

# 配置Ribbon调用user-app和order-app服务时使用我们自定义灰度轮询算法
user-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
order-app:
  ribbon:
    NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule

配置网关Nacos配置文件(gateway-app.yaml)

kerwin:
  tool:
    gray:
      gateway:
        # 是否开启灰度发布功能
        enabled: true
        # 自定义灰度版本请求头
        grayHeadKey: gray
        # 自定义灰度版本请求头匹配值
        grayHeadValue: gray-996
        # 使用灰度版本IP数组
        grayIPList:
          - '127.0.0.1'
        # 使用灰度版本城市数组
        grayCityList:
          - 本地
<think>嗯,用户问的是关于Spring Cloud Gateway的自定义断言。我需要先回忆一下Gateway的基本概念,断言的作用是什么。断言主要用于路由匹配,根据请求的不同条件来决定是否路由到某个服务。 首先,应该解释什么是自定义断言,可能用户已经了解内置的断言,比如Path、Query这些,但需要扩展自己的逻辑。然后,步骤应该是如何创建自定义断言,可能需要实现特定的接口,比如RoutePredicateFactory。 接下来,要考虑用户可能的疑问点,比如如何注册自定义断言到Spring容器,配置文件中如何引用。还要注意命名的规范,因为Spring Cloud Gateway的断言工厂命名有特定规则,比如XXXRoutePredicateFactory,配置的时候用XXX=参数。 可能需要一个具体的例子来说明,比如基于请求时间的断言。这样用户更容易理解。同时,要提醒注意事项,比如配置的key要和工厂名前缀一致,确保Spring能够正确识别。 另外,是否需要提到版本兼容性?或者常见的错误,比如配置错误导致断言不生效?可能用户在实际操作中会遇到这些问题,所以注意事项部分可以包括这些内容。 最后,总结一下自定义断言的应用场景,比如灰度发布、特定Header校验等,让用户知道什么时候需要用到它。确保整个回答结构清晰,步骤明确,示例具体,注意事项全面。</think>Spring Cloud Gateway 的自定义断言是用于扩展路由匹配规则的核心机制,下面通过结构化的步骤为您讲解实现原理和操作方法: ### 一、断言的作用原理 1. **路由匹配**:每个路由可配置多个断言,全部满足时触发路由 2. **执行顺序**:位于过滤器链之前,先进行请求匹配,再执行过滤逻辑 3. **内置断言**:包含 Path、Method、Header 等 12 种官方实现 ### 二、实现自定义断言(3个核心步骤) #### 1. 创建断言工厂类 ```java @Component public class CustomTimePredicateFactory extends AbstractRoutePredicateFactory<CustomTimePredicateFactory.Config> { // 必须的构造函数 public CustomTimePredicateFactory() { super(Config.class); } // 断言逻辑实现 @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { LocalTime now = LocalTime.now(); return now.isAfter(config.start) && now.isBefore(config.end); }; } // 配置参数类 @Validated public static class Config { private LocalTime start; private LocalTime end; // 必须的getter/setter } // 参数绑定(关键步骤) @Override public List<String> shortcutFieldOrder() { return Arrays.asList("start", "end"); } } ``` #### 2. 配置路由规则(application.yml) ```yaml spring: cloud: gateway: routes: - id: time_route uri: http://example.org predicates: - name: CustomTime # 自动匹配XXXRoutePredicateFactory args: start: 09:00:00 end: 17:00:00 ``` #### 3. 命名规范要求 - 类名必须遵循 `[YourName]RoutePredicateFactory` 格式 - 配置文件中的 `name` 值对应类名前缀(如 `CustomTime` 对应 `CustomTimeRoutePredicateFactory`) ### 三、典型应用场景 1. **灰度发布控制**:基于设备类型或用户分组路由 2. **地域路由**:根据 IP 解析的地理位置分配服务 3. **协议版本校验**:检查特定 header 中的版本号 4. **流量采样**:按百分比随机路由到新版本服务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值