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

前言

前阵子时间和朋友聊天,他们有个sass微服务,因为之前拆分过细,导致服务不仅调用链路过长,而且浪费服务资源,他们后面做了服务合并的重构,并即将上线。他觉得上线不能直接把线上的租户都全切到重构版的sass微服务,而是需要实现如下的效果 他就问我说,有没有啥开源平台可以快速支持,因为之前时间都耗费在重构业务上,这块就没考虑周全,现在临近上线,预留的时间不多。后面和他细聊,得知他们这套sass服务,租户不多,其次他们微服务API网关是springcloud gateway。了解到这个信息后,我就跟他说直接拿API网关稍微改造一下,就可以达到他目前想要的效果。下面就来聊聊如何利用springcloud gateway实现简易版灰度路由

实现关键

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

实现步骤

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

1、项目POM引入相关GAV

xml 复制代码
   <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、自定义断言工厂

java 复制代码
@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、配置断言工程自动装配

java 复制代码
@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文件开启服务发现路由定位器

yaml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true

测试灰度路由

1、测试微服务comsumer1

a、测试配置

yaml 复制代码
spring:
  application:
    name: ${APPLICATION_NAME:comsumer}
  profiles:
    active: eureka

b、编写测试控制器

java 复制代码
@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、测试配置

yaml 复制代码
spring:
  application:
    name: ${APPLICATION_NAME:otherComsumer}
  profiles:
    active: eureka

b、编写测试控制器

java 复制代码
@RestController
@RequestMapping("echo")
public class EchoController {

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

}

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

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

yaml 复制代码
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的值时

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

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

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

yaml 复制代码
spring:
  cloud:
    gateway:
      properties-route-definition-locator:
        load:
          first: false

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

同时路由做如下配置

yaml 复制代码
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的数值调低。我们再验证下

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

总结

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

demo链接

github.com/lyb-geek/sp...

相关推荐
MrSYJ15 小时前
AuthenticationEntryPoint认证入口
java·spring cloud·架构
银迢迢19 小时前
SpringCloud微服务技术自用笔记
java·spring cloud·微服务·gateway·sentinel
弈芯2 天前
SpringCloud微服务拆分最佳实践
spring cloud
麦兜*2 天前
【Prometheus】 + Grafana构建【Redis】智能监控告警体系
java·spring boot·redis·spring·spring cloud·grafana·prometheus
sniper_fandc3 天前
Spring Cloud系列—SkyWalking告警和飞书接入
spring cloud·skywalking
abigalexy4 天前
深入图解Spring Cloud底层设计
spring·spring cloud
楠有枝5 天前
普通用户使用docker命令
spring cloud·docker·eureka
孤狼程序员5 天前
【Spring Cloud 微服务】2.守护神网关Gateway
spring cloud·微服务·gateway
朱皮皮呀5 天前
Spring Cloud——服务注册与服务发现原理与实现
运维·spring cloud·eureka·服务发现·php
朱皮皮呀6 天前
微服务流量分发核心:Spring Cloud 负载均衡解析
spring cloud·微服务·负载均衡