Spring Cloud GW路由规则

261 阅读6分钟

       “路由”无疑是边缘网关最基础的功能,路由规则也是实践中首要配置项,无论跑demo还是演示poc,今天来分享一下spring cloud gateway 的路由规则。先从解读源码分析SCG内置的路由规则以及如何生效的原理,然后会有一个sample来实践几个路由规则的语义(sample 分支:gitee.com/wlscode/scg… 到SCG中时发现SCG的路由规则的语义并不能覆盖envoy的语义,换句话说,案例中envoy的配置无法在SCG中通过配置route predicate来实现。

1. 内置的路由规则

       SCG中的路由规则由一组Predicate来表达的,如上图所示,请求先经过一系列的predicate来匹配路由。在org.springframework.cloud.gateway.handler.predicate包下,内置了许多现成的路由规则,以3.1.0版本为例,大概内置了十几个:

       对其进行归类之后,发现涵盖了Path、Host、Method、RemoteAddr、Cookie、Query、甚至Weight,开箱即用就能够满足绝大部分场景了。

2. 一条简单的规则

假设我现在配置一个Path和Method的组合路由规则,如下面yml配置:

spring.cloud.gateway:
  routes:        
    - id: route-1          
      uri: http://localhost:8080          
      predicates:            
        - Path=/p/**            
        - Method=post

这是一个足够简单的配置,启动生效后,所有post请求到/p开头的Path都可以成功路由到 http://localhost:8080. 运行后发现是一个PathRoutePredicateFactory和一个MethodRoutePredicateFactory ,其实从配置命名也可推断。

3. 路由规则加载原理

       那么,配置是如何加载生效的呢?这要从 GatewayAutoConfiguration 入手。这是SCG众多自动配置类中的一个,承担了SCG中Route相关Bean的初始化功能,包括各种路由规则的XXXRoutePredicateFactory,各种网关插件XXXGatewayFilterFactory,route刷新监听RouteRefreshListener,以及各种加载route配置的功能Bean -- 例如 RouteDefinitionLocator、RouteDefinitionRepository等。具体的可以看官方代码:github.com/spring-cloud/spring-cloud-gateway。上文配置文件的Route是有其中一个叫做PropertiesRouteDefinitionLocator来加载的:

       严格的说并不是他来加载,而是他包含一个叫做GatewayProperties的bean,打开这个类发现该类有一个以spring.cloud.gateway 为前缀的ConfigurationProperties注解,根据springboot的运行机制他其实就会自动从配置文件中读取对应的配置并初始化该bean中了。此时加载了配置到GatewayProperties的bean中,要转化为SCG内部运行的route规则,还需要几个步骤:

**首先,**RouteDefinitionRouteLocator上场,他会处理配置中的predicate和fitlers使其转化为真正的PredicateFactory和FilterFactory:

其次,CachingRouteLocator上场,该类初始化之后会把之前处理的predicate和fitler转化了真正的Route:

**最后,**启动完成之后 RouteRefreshListener 会publish RouteRefreshsEvent。用一个类图来表达下关系如下:

4. Predicate运行时

当路由规则加载完成并且生效之后,每一个请求进来都会对route列表挨个进行predicate,结果有两种:

  • 找到第一个符合的路由规则

  • 遍历完route列表没有找到匹配的规则

针对每一个route,会遍历属于该route的所有predicate,当且仅当所有predicate.test都是true才被认为该route匹配当前请求。

5. PathRoutePredicateFactory 解读

下面以PathRoutePredicateFactory为例来解读下predicate的运行过程,所谓的“核心”代码就如下几行:

PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
PathPattern match = null;
for (int i = 0; i < pathPatterns.size(); i++) 
{  
    PathPattern pathPattern = pathPatterns.get(i);  
    if (pathPattern.matches(path)) {    
        match = pathPattern;    break;  
    }
}

核心判断:org.springframework.web.util.pattern.PathPattern#matches。仔细翻阅,该类针对web请求按照/分割,每一段对应一个PathElement来判断是否匹配。

PathElement有很多种实现类,有literal匹配的,有通配符匹配的,也有regex正则匹配的,等等。看上去功能很丰富,但其实regex的匹配支持非常有限,如果想当然的认为可以配置java中的正则表达式,那会踩大坑的。

下面以一个sample来解释下怎么踩坑的

6. 踩坑 sample 演示

之前收集各个业务小组gw需求时,其中有几个非常细腻的路由规则配置,比如三条路由规则:

/r1/**

需要带token=foo才能访问

/r2/**

需要带token=bar才能访问,并且给upstream自动带一个header(p=r2)

其他

给upstream带上一个header(p=other)

于是我就想当然的配置:

routes:
  - id: route-1
    uri: http://localhost:8080
    predicates:
      - Path=/r1/**
      - Header=token,bar
  - id: route-2
    uri: http://localhost:8080
    predicates:
      - Path=/r2/**
      - Header=token,foo
    filters:
      - AddRequestHeader=p,r2
  - id: route-other
    uri: http://localhost:8080
    predicates:      
      - Path=/(r[^12]|[^r].*)/**
    filters:
      - AddRequestHeader=p,other

当好不容易写完“其他”路由的正则表达式/(r[^12]|[^r].*)/**之后以为大功告成,其实运行就报错!!而这个正则表达式/(r[^12]|[^r].*)/**在java里运行妥妥的是ok的,那为什么在SCG的路由配置中没有按照理想的work呢?原因就是上文有提到的过的正则匹配运算:RegexPathElement,如果对这个类不熟悉,那可以参考org.springframework.util.AntPathMatcher ,他俩如出一辙,只能做到有限的正则匹配,比如".?*",而上文这么复杂的正则表达式是无法正确匹配的。

无奈之下,做出如下调整:

routes:
  - id: route-1
    uri: http://localhost:8080
    predicates:
      - Path=/r1/**
      - Header=token,bar
  - id: route-2
    uri: http://localhost:8080
    predicates:
      - Path=/r2/**
      - Header=token,foo
    filters:
      - AddRequestHeader=p,r2
  - id: route-other
    uri: http://localhost:8080
    predicates:      
      - Path=/**
    filters:
      - AddRequestHeader=p,other

上面调整之后是可以正确运行的,但是“有损”运行,假设当 /r1/hello过来的时候,我们的预期是希望返回错误,因为没有按照route-1带上header(token=bar),但实际上请求会被route-other 匹配到。而另一方面,路由的顺序也是有讲究的,如果把router-order放到第一个,那么所有请求都会直接match这个路由了,剩下两个路由形同虚设。Sample的代码可以这里获取:gitee.com/wlscode/scg…

7. envoy 实现参考

最后,如果一定不能接受上面”有损“配置,我们可以在SCG网关服务前置一个envoy proxy(nginx应该也可以),可以把部分功能移到proxy中实现,而scg可以做一些更加“业务”一点的职责。envoy的route配置:

routes:
  - match:
      prefix: "/r1"
      headers:
        name: "token"
        exact_match: "foo"
    route:
      cluster: 
        mock_server
  - match:
      prefix: "/r2"
      headers:
        name: "token"
        exact_match: "bar"
    route:
      cluster: mock_server
    request_headers_to_add:
      - header:
          key: "path"
          value: "r2"
  - match:
      safe_regex:
        google_re2: {}
        regex: "^/(r[^123]|[^r].*)/.*"
    route:
      cluster: mock_server
    request_headers_to_add:
      - header:
          key: "path"
          value: "r2"          

​详细代码:

gitee.com/wlscode/scg…

验证步骤:

1.启动envoy -c envoy.yml

2.启动mockServer

3.验证请求

curl http://localhost:10001/r1/hello --不匹配路由,404

curl -H 'token:foo' http://localhost:10001/r1/hello --匹配第一个路由

curl -H 'token:foo' http://localhost:10001/r2/hello --不匹配路由,404

curl -H 'token:bar' http://localhost:10001/r2/hello --匹配第二个路由

curl -H 'token:bar' http://localhost:10001/r3/hello --匹配第三个路由

​对应envoy日志:

从mock_server查看打印的request header分别带上了对应路由配置的p: