美文网首页
聊聊如何利用springcloud gateway实现简易版灰度

聊聊如何利用springcloud gateway实现简易版灰度

作者: linyb极客之路 | 来源:发表于2023-11-20 10:24 被阅读0次

    前言

    前阵子时间和朋友聊天,他们有个sass微服务,因为之前拆分过细,导致服务不仅调用链路过长,而且浪费服务资源,他们后面做了服务合并的重构,并即将上线。他觉得上线不能直接把线上的租户都全切到重构版的sass微服务,而是需要实现如下的效果


    ab1a435bbb7c299052b64b241e57ffba_d882651c1c87be51bd622ae9247b2d62.png

    他就问我说,有没有啥开源平台可以快速支持,因为之前时间都耗费在重构业务上,这块就没考虑周全,现在临近上线,预留的时间不多。后面和他细聊,得知他们这套sass服务,租户不多,其次他们微服务API网关是springcloud gateway。了解到这个信息后,我就跟他说直接拿API网关稍微改造一下,就可以达到他目前想要的效果。下面就来聊聊如何利用springcloud gateway实现简易版灰度路由

    实现关键

    springcloud gateway 自定义断言工厂 + 开启服务发现路由定位器 + PropertiesRouteDefinitionLocator 生成的route与DiscoveryClientRouteDefinitionLocator生成route path映射保持一致

    实现步骤

    注: 本示例注册中心使用eureka,其他注册中心也可以

    1、项目POM引入相关GAV

       <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
    

    2、自定义断言工厂

    @Slf4j
    public class ParamRoutePredicateFactory
            extends AbstractRoutePredicateFactory<ParamRoutePredicateFactory.Config> {
    
        public static final String PARAM_KEY = "param";
    
        public static final String PARAM_VALUES = "values";
    
        public static final String SEPARATOR = "&";
    
        public ParamRoutePredicateFactory() {
            super(Config.class);
        }
    
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList(PARAM_KEY,PARAM_VALUES);
        }
    
        @Override
        public ShortcutType shortcutType() {
            return ShortcutType.DEFAULT;
        }
    
        @Override
        public Predicate<ServerWebExchange> apply(Config config) {
            return exchange -> isHitTargetParam(config, exchange);
        }
    
        private boolean isHitTargetParam(Config config, ServerWebExchange exchange) {
            boolean hasParamkey = HttpRequestParserUtils.hasKey(config.param.toLowerCase(), exchange);
            if(hasParamkey){
                String value = HttpRequestParserUtils.parse(config.param.toLowerCase(), exchange);
                if(StringUtils.hasText(config.values) && config.values.contains(SEPARATOR)){
                    String[] valueArr = config.values.split(SEPARATOR);
                    for (String targetValue : valueArr) {
                        if(targetValue.equals(value)){
                            log.info(">>>>>>>>>>>>>>>>>>>> Request Key --> 【{}】 Hit Value --> 【{}】 In Target Values 【{}】", config.param,value, config.values);
                            return true;
                        }
                    }
                }
    
            }
            return false;
        }
    
        @Validated
        public static class Config {
    
            @NotEmpty
            private String param;
    
            private String values;
    
            public String getParam() {
                return param;
            }
    
            public Config setParam(String param) {
                this.param = param;
                return this;
            }
    
            public String getValues() {
                return values;
            }
    
            public Config setValues(String values) {
                this.values = values;
                return this;
            }
    
            @Override
            public String toString() {
                return "Config{" +
                        "param='" + param + '\'' +
                        ", values=" + values +
                        '}';
            }
        }
    

    3、配置断言工程自动装配

    @Configuration
    @ConditionalOnProperty(name = "spring.cloud.gateway.ext.enabled", havingValue = "true",matchIfMissing = true)
    @AutoConfigureBefore({ GatewayDiscoveryClientAutoConfiguration.class})
    @ConditionalOnClass(DispatcherHandler.class)
    public class GatewayAutoExtConfiguration {
    
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "spring.cloud.gateway.properties-route-definition-locator.load.first", havingValue = "true",matchIfMissing = true)
        public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(
                GatewayProperties properties) {
            return new PropertiesRouteDefinitionLocator(properties);
        }
    
        @Bean
        @ConditionalOnMissingBean
        public ParamRoutePredicateFactory paramRoutePredicateFactory(){
            return new ParamRoutePredicateFactory();
        }
    
    }
    
    

    注: 这边有些细节点说明一下,该配置先于GatewayDiscoveryClientAutoConfiguration装配,主要是实现PropertiesRouteDefinitionLocator 比DiscoveryClientRouteDefinitionLocator优先加载,为啥这么做,后面说

    4、在application.yml文件开启服务发现路由定位器

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
              lower-case-service-id: true
    

    测试灰度路由

    1、测试微服务comsumer1

    a、测试配置

    spring:
      application:
        name: ${APPLICATION_NAME:comsumer}
      profiles:
        active: eureka
    

    b、编写测试控制器

    @RestController
    @RequestMapping("echo")
    public class EchoController {
    
        @GetMapping("{message}")
        public String echo(@PathVariable("message") String message){
            System.out.println("comsumer:" + message);
            return "comsumer :" + message;
        }
    
    }
    

    2、测试微服务comsumer2

    a、测试配置

    spring:
      application:
        name: ${APPLICATION_NAME:otherComsumer}
      profiles:
        active: eureka
    

    b、编写测试控制器

    @RestController
    @RequestMapping("echo")
    public class EchoController {
    
        @GetMapping("{message}")
        public String echo(@PathVariable("message") String message){
            System.out.println("otherComsumer:" + message);
            return "otherComsumer :" + message;
        }
    
    }
    
    

    注:这个两个服务主要用来模拟新老集群数据

    3、网关添加测试路由配置

    spring:
      cloud:
        gateway:
          routes:
            - id: route-springboot-gray-comsumer-to-other-comsumer
              uri: http://localhost:8083
              predicates:
                - Path=/comsumer/**
                  ## 多个租户用&分割
                - Param=tenantId,10000&10001&10002
              filters:
                - StripPrefix=1
              order: 0
    

    注: 这个配置心细的朋友,可能会发现猫腻了。这个PATH和开启服务发现路由定位器生成的PATH是一样,我们再来说下为啥上面实现PropertiesRouteDefinitionLocator 比DiscoveryClientRouteDefinitionLocator优先加载,因为路由定位器产生的route是有顺序性,而当PropertiesRouteDefinitionLocator 和DiscoveryClientRouteDefinitionLocator配置的PATH一样时,如果DiscoveryClientRouteDefinitionLocator优于PropertiesRouteDefinitionLocator加载,就会导致访问相同路径时,会优先访问DiscoveryClientRouteDefinitionLocator生成的route,就不会去走我们自定义配置的route。不过这个结论为时尚早,留个悬念,待会说明

    4、测试

    1、当我们请求头、cookie、query不加tenantId参数或者tenantId不为测试10000&10001&10002的值时

    a15780c503a66307ca310fd4c38eb405_fba0c19ec06dac87124ff7b2142632ec.png fb33dbe2c77714bdd4f7879bb4e18932_9bac5f29f6a3366bbb796ba1ebed5ccb.png

    2、当tenantId满足10000&10001&10002的其中任意值时

    10b568aef2c87265e01e761c72ff91e4_4cbc765ad19e675cb5b94c8e08f9958f.png

    可以发现已经路由到我们配置的地址

    3、当我们对网关做如下配置

    spring:
      cloud:
        gateway:
          properties-route-definition-locator:
            load:
              first: false
    

    该配置主要是为了让我们自定义的PropertiesRouteDefinitionLocator 的BEAN失效,这样他就会按默认的加载逻辑,即DiscoveryClientRouteDefinitionLocator会先于PropertiesRouteDefinitionLocator 加载

    同时路由做如下配置

    spring:
      cloud:
        gateway:
          routes:
            - id: route-springboot-gray-comsumer-to-other-comsumer
              uri: http://localhost:8083
              predicates:
                - Path=/comsumer/**
                ## 多个租户用&分割
                - Param=tenantId,10000&10001&10002
              filters:
                - StripPrefix=1
              order: -1000
    
    

    即将order的数值调低。我们再验证下

    a98355148c76fbff4221e821995b89fe_84cf066fd84b87d1e31f8b0be22bd887.png

    会发现效果和我们之前演示的效果是一样的。其实这边实现路由的关键点,是抓住route的顺序性,相同路径,谁先加载,谁先路由。所以我实现PropertiesRouteDefinitionLocator 比DiscoveryClientRouteDefinitionLocator会优先加载,就是为了实现当path一样时,PropertiesRouteDefinitionLocator 生成的route都比DiscoveryClientRouteDefinitionLocator生成route优先,当然也可以通过配置order改变这个顺序

    总结

    本示例主要讲解如何利用springcloud gateway实现简易版灰度路由,不过该实现比较适用于灰度规则比较简单的场景。如果需要复杂规则,就需要深层次的定制,或者采用用istio来实现也是一个挺好的选择

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-gateway-simple-gray

    相关文章

      网友评论

          本文标题:聊聊如何利用springcloud gateway实现简易版灰度

          本文链接:https://www.haomeiwen.com/subject/jokvwdtx.html