走一遍SCG常用内置功能,竟意外发现bug了 前几篇我们就SCG构建基础、启动原理、路由规则以及如何接管webflux请求做了分析,本篇我们通过一个demo来实践一遍SCG常用的内置功能。demo体验过程并没有那么顺利,因为竟意外发现SCG源码的一个隐秘bug,它不仅使得启动慢,而且当配置某个predicate之后竟然调用会因超时而失败,神雕探寻了很久才定位到框架中那行“罪魁祸首”的代码,当然避开它的方案还是很简单的。
既然是demo,我们先展示下demo的工程结构
项目主体结构主要三部分:
- gateway-service: 当然是提供网关服务,所有功能配置都在application-full.yml里,另外Predicate包里两个类主要是来解决开篇说到的SCG的bug
- mockserver: 一个mock upstream的服务,在RouteTestController里mock我们需要的一些api
- mockclient: 主要是用来模拟发请求的,有些场景需要快速的发请求,这在后续限流相关的文章分享中还会有用。
另外,项目还依赖:
- redis: 我们demo中引入了基于redis的限流组件,可以本地起个容器
- postman: 主要发请求时方便修改header、param、host、method等等
ok,现在开始demo的第一部分,验证各类型的RoutePreidcate,组合成一个复杂的路由规则,当然这只是为了体验一遍这些规则的工作效果,在实际项目中不太可能有这么复杂的规则集中在一个路由配置上 :),再附一次SCG内置的Predicate图:
如上图,大概有9种类型的内置predicate,通过名字前缀(后缀都是PredicateFactory结果) 基本也能明白大概功能。根据SCG的约定,我们以静态配置的方式写在application-full.yml文件里,只需要配前缀即可生效
1)Datetime 类型
主要根据时间条件来匹配路由规则,比如618期间搞活动才有效的一个api,可以配置一个between,不在这个Between范围内的时间无法正常路由过去 - Between=2022-06-01T00:00:00.020+08:00[Asia/Shanghai],2022-06-18T23:59:59.020+08:00[Asia/Shanghai] PS: 这里的配置需要带上时区
2)Cookie 类型
根据配置的Cookie来路由(支持正则表达式),可以配置多个,比如 - Cookie=c,c_\d+,只有当请求Cookie中带有name为c值以”c_“为前缀带有若干数字才符合路由匹配。
3)Header 类型
根据配置的header来路由(支持正则表达式),可以配置多个,比如 - Header=h,\d+,只有当请求的Header中带有name为h 值为数字才符合路由匹配
4)Host 类型
根据配置的Host来路由,支持通配符,也支持配置多个值用逗号隔开。比如 - Host=testhost.com,只有当请求带有Host为testhost.com时候才符合路由匹配。PS: 这个case会涉及到跨域问题,可以通过SCG的跨域配置或者前置一个proxy(比如envoy)来解决。
5)Method 类型
根据配置的http方法来路由,可以配置多个值用逗号隔开。比如 - Method=GET,POST, 只有GET和POST方法请求才符合路由匹配。
6)Path 类型
根据配置的Path来路由,支持通配符(或者说支持PathMatcher规则,类似AntPathMatcher的功能,这个在”Spring Cloud GW路由规则“一文中有详细叙述),可以配置多个用逗号隔开即可。多说一句,这是使用最多的一个路由匹配规则,绝大部分场景下系统都以不同的Path来路由就够了,相反,很少有路由不需要配置Path规则的。比如:- Path=/p1/**, /p2/** 表示以/p1或者/p2为前缀的请求才符合路由匹配。
7)Query 类型
根据配置的Query参数匹配,支持正则表达式,也支持配置多个。比如 - Query=q,1,只有当请求参数中带有name为q值为1才符合路由匹配
8)RemoteAddr
根据请求的来源地址来判断是否满足路由规则,可配置多个ipv4或者ipv6,支持子网掩码,比如 - RemoteAddr=192.168.0.1/16(其中192.168.0.1是IP地址,16是子网掩码)。这里需要特别注意,很多时候,特别是当网关前面有proxy时(比如前置envoy或者nginx),网关拿到remoteAddr往往是proxy的地址,而不是真正请求来源地址,所以实际场景中我们会使用”x-forwarded-for“作为判断依据.
9)Weight
权重路由判断,两个路由配置同一个组分别设置不同的比值,那么会按比例进行路由,在某些场景下比较有用的,马上有demo演示。
首先针对前八个类型的Predicate,我们配置这样一个路由:
- id: p-route
uri: http://localhost:8080
predicates:
- Path=/r1/**
- Method=GET
- Header=h,1
- Cookie=c,c\d+
- Query=q,1
- Between=2022-05-01T11:08:08.020+08:00[Asia/Shanghai],2022-05-30T11:15:08.020+08:00[Asia/Shanghai]
- XForwardedRemoteAddr=192.168.1.1/16
- Host=testhost.com
有了前面的介绍,很好理解其含义,当一个请求满足以下条件时才匹配到这个路由规则:
- path以 /p1 开头
- 方法为GET
- 带有名字为h值为1的Header
- 带有名字为c值为c_开头多个数字结尾的Cookie
- 带有名字为q值为1的参数
- 时间在2022-05-01T00:00:00.020+08:00[Asia/Shanghai],2022-05-30T23:59:59.020+08:00[Asia/Shanghai]之间
- x-forwarded-for获取到满足”192.168.1.1/16“的来源地址
- 请求中带有源Host为testhost.com
另外,因为有了Host条件,我们还需要加下跨域配置:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
看运行效果:
- 启动redis: docker run --name csg-demo-redis -p6379:6379 -d redis:latest redis-server
- 启动 MockserverApplication(端口 8080)
- 启动 GatewayServiceApplication(-Dspring.profiles.active=full,端口为10001)
- 启动postman,请求:
有了以上红框的匹配条件,以及隐含条件(今天是2022.05.29),请求是可以正确路由的,修改任何一个条件不满足规则的话 比如Header或者Cookie或者Method,请求就会失败。 正确路由之后,我们可以看到mock server打印的参数:
这里有个小插曲,眼尖的您可能已经发现 XForwardedRemoteAddr 这个Predicate并不是SCG自带的。之前说过,我们经常会用x-forwarded-for代替remote addr的判断,所以神雕本来是想再SCG内置的 RemoteAddrRoutePredicateFactory 中使用框架自带的 XForwardedRemoteAddressResolver 来替换默认的RemoteAddressResolver。然而”卡壳“了好久,先是启动超慢,启动之后呢,请求超时失败,曲折的过程就不浪费您时间了,直接说原因和解决方案。
SCG 内置的 XForwardedRemoteAddressResolver 解析Address时,第一步提取x-forwarded-for的ip地址写死了用", "作为分割获取ip列表,很多时候一个请求经过多个server跳转(比如从外网到proxy再到网关),每一跳的ip地址之间直接逗号分割,并不会有空格,于是上面的extract就不正确。
这个错误会导致后续获取Address超级慢,结果还不正确,有兴趣可自行debug研究下 XForwardedRemoteAddressResolver。 解决方案也是很简单的,基于内置的 RemoteAddrRoutePredicateFactory 扩展了一个XForwardedRemoteAddrRoutePredicateFactory,并且内部使用修正后的Resolver
https://gitee.com/wlscode/scg/blob/route_sample/gateway-service/src/main/java/ws/atc/gatewayservice/predicate/ScgXForwardedRemoteAddressResolver.java
@Bean("remoteAddressResolver")
public ScgXForwardedRemoteAddressResolver xForwardedRemoteAddressResolver(){
return ScgXForwardedRemoteAddressResolver.trustAll();
}
public class XForwardedRemoteAddrRoutePredicateFactory extends RemoteAddrRoutePredicateFactory {
@Resource(name = "remoteAddressResolver")
private RemoteAddressResolver xForwardedRemoteAddressResolver;
@Override
public Predicate<ServerWebExchange> apply(Config config) {
config.setRemoteAddressResolver(xForwardedRemoteAddressResolver);
return super.apply(config);
}
}
接下来演示下 WeightRoutePredicateFactory ,直接上配置:
- id: weightRoute1
uri: http://localhost:8080
predicates:
- Path=/w/**
- Weight=group1,9
filters:
- RewritePath=/w/(?<remaining>.*), /w9/$\{remaining}
- id: weightRoute2
uri: http://localhost:8080
predicates:
- Path=/w/**
- Weight=group1,1
filters:
- RewritePath=/w/(?<remaining>.*), /w1/$\{remaining}
解释一下, weightRoute1 和 weightRoute2 都是对请求 /w 为前缀的 Path 进行路由,针对 group1 进行权重分配,其中 9/10 分配给 weightRoute1,1/10 分配给 weightRoute2。 另外,配合一个 RewritePath 的组件进行重写请求路径,weightRoute1 会把请求指向 upstream 的 /w9/**, weightRoute2 会把请求指向 upstream 的 /w1/**. 当然,upstream(即我们的mockserver)是有两个 api 的,可以看做是不同算法的验证,或者abtest
@RequestMapping("/w1/hello")
public String w1(){
return "/w1/hello";
}
@RequestMapping("/w9/hello")
public String w9(){
return "/w9/hello";
}
运行效果: 我们运行mockclient,模拟发起100次请求 /w/hello, 按权重分配会有 90% 左右的请求打到mockeserver的 /w9/hello:
实际运行如上图,87打到/w9,13打到了/w1,也接近理论比例。
到此,常用的 SCG 内置 predicate 体验结束,相信您有比较直观的感受,在实际场景中能够选择使用。其中我们定制了 XForwardedRemoteAddr,看看代码,从中也能 get 到扩展 SCG 的便捷性。
最后,我们来体验 SCG 另一趴:Filter。从流程上来说,当 Predicate 匹配到了一个路由,接下来请求就会开始走到匹配路由的 Fitler 链,这才是 SCG 能力插拔的体现。Filter 分为 Global 和 非 Global两种,global 类型的filter无论什么请求都会经过,非 global 的fitler 配置才会起作用。本文我们主要针对非 global 的,内置的 Fitler 种类非常丰富:
从上图 Fitler 的名字前缀可以直观的了解到,我们不仅可以从请求Path,请求头、请求参数、请求host、请求 body 来干预流程,也能从 响应头、响应参数、响应 body 、响应状态码等进行干预,甚至对请求做 熔断、限流等等干预。神雕选择从 Path,Request,Response,Rate Limit 四个方面来演示下内置 Fitler 的功能。
直接上配置:
- id: f-route
uri: http://localhost:8080
predicates:
- Path=/other/**
filters:
- StripPrefix=1
- PrefixPath=/prefix
- RewritePath=/rr/(?<remaining>.*), /$\{remaining}
- AddRequestHeader=x-req-id,abc
- AddRequestParameter=req_of,p1
- MapRequestHeader=x-id, uname
- RemoveRequestHeader=rrHeader
- PreserveHostHeader
- AddResponseHeader=x-resp-id,123
- RemoveResponseHeader=rrHeader
- DedupeResponseHeader=drh,RETAIN_UNIQUE
- name: RequestRateLimiter
args:
key-resolver: "#{@pathKeyResolver}"
redis-rate-limiter.replenishRate: 10 #令牌每秒填充速度
redis-rate-limiter.burstCapacity: 20 #桶大小
redis-rate-limiter.requestedTokens: 1 #默认是1,每次请求消耗的令牌数
这是一个新的路由,处理 /other/** 匹配的请求,它有 12 个 Fitler
- Path 相关的 3 个:
- StripPrefix: 值是一个数字,用来移除 Path 的前缀部分,比如/a/b/c,当值为1时或把/a移除,值为2时会把 /a/b移除
- PrefixPath: 值为前缀字符串,会加到请求 Path 的前缀里,比如配置了 /prefix1, 那么 /hello 请求会改写为 /prefix1/hello
- RewritePath: 直接重写 Path,可以把 Path A 重写为 PathB 另外,顺序很重要,因为 filter 链是依次执行,上个执行的输出会影响下一个执行的输入。
按照我们上面 demo 里的配置为例,假设请求 /other/rr/hello 进来,首先经过 StripPrefix(=1)会把/other删除,请求 Path 就变成了 /rr/hello, 然后在经过 PrefixPath(=prefix)之后 Path 就变成了 /prefix/rr/hello,最后经过 RewritePath 重写之后变成了 /prefix/hello
2) Request 相关的 5 个
- - AddRequestHeader 添加请求头 x-req-id=abc
- - AddRequestParameter 添加请求参数 req_of=p1
- - MapRequestHeader 把请求头 x-id 映射为 uname
- - RemoveRequestHeader 删除请求头 rrHeader (如果存在的话)
- - PreserveHostHeader 保留请求头 Host,不用配置默认是保留的
3)Response 相关的 3 个
- - AddResponseHeader 添加响应头 x-resp-id=123
- - RemoveResponseHeader 删除响应头 rrHeader(如果存在的话)
- - DedupeResponseHeader 保证响应头 drh 没有重复值
4)熔断限流相关 1 个
- 配置了一个基于 redis 的请求限流组件,基于 pathKeyResover 进行 10 QPS 限流,应对突发流量 20 QPS
运行: postman 发出请求,带上测试 header
mockserver 收到请求 Path 已经被改为 /prefix/hello,并且带有 Fitler 加上的 Header 和 param
postman 收到的 Response Header