如果你现在所接触的项目是基于微服务架构,如果碰巧你们项目使用的是springcloud alibaba,再碰巧网关是用的Spring Cloud Gateway,那不妨停留片刻,了解一下这些概念挺好。
注:如果大家没意见,本文就用SCG简代替Spring Cloud Gateway了。
- 清楚为什么选择SCG?
- SCG的请求流程?
- SCG的核心组件有哪些?
1. 为什么选择SCG
As we known,SCG是SpringCloud生态中的网关标配,基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty,非阻塞模型,性能更好,下图是来自官网图
做了一个demo测试一下是不是真的比传统的MVC更快呢?基于这SpringWebFlux和SpringMVC两个框架分别写了一下简易测试接口:
/************webflux版本*******/
@RestController
@RequestMapping("demo")
public class WebFluxDemoController {
@GetMapping("/hello")
public Mono<String> sayHelloWebflux() {
return Mono.just("WebFlux say hello").delayElement(Duration.ofMillis(50));
}
}
/ *************mvc版本********/
@RestController
@RequestMapping("demo")
public class MvcDemoController {
@GetMapping("/hello1")
public String sayHelloMvc() throws Exception {
TimeUnit.MILLISECONDS.sleep(50);
return "MVC say hello";
}
}
使用jmeter压测,100线程并发,不同电脑配置可能结果不一样,我的是Intel i5,8GB内存,测试结果如下图,看起来性能有近一倍的提升,最大请求延时也远低于传统MVC,大家有兴趣也可以自己测试一下。
2. 核心请求流程
注:来自官网,步骤说明:
- 客户端向网关Gateway发送请求;
- Gateway Handler Mapping阶段确定当前请求与路由匹配(通过断言predicate匹配到具体的路由配置),随后将其发送到Gateway web handler处理;
- 由 Gateway web handler 创建过滤器链并调用;
- 通过特定于请求的 Fliter 链运行请求,先执行所有“pre”过滤器逻辑;
- 进行代理请求,通过RPC调用后端的微服务;
- 执行“post”过滤器逻辑;
- 将处理完后的响应体封装返回给客户端。
3. 核心的组件
Route(路由):
可以这么说,路由是网关最为重要的核心组件之一,我们网关相关的开发工作就主要围绕着路由来转的,比如对xx路由需要鉴权,对xx路由需要黑名单认证,对xx路由需要加解密放行,对xx路由需要限流等等。所以先看一下路由的组成:
1. id:路由唯一标识,区别于其他的route
2. url: 路由指向的目的地URL,客户端请求最终被转发到的微服务
3. order: 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高
4. predicate:断言的作用是进行条件判断,只有断言为true,才执行路由
5. filter: 过滤器用于修改请求和响应信息
在生产环境中,我们的路由是配置到Nacos中,并通过进行动态路由更新,比如现在要路由到我们的用户服务,信息就是这么配置的(后面有空单独写一篇配置明细说明):
{
"id": "user-router",
"order": 0,
"predicates": [
{
"args": {
"pattern": "/user-web/**"
},
"name": "Path"
}
],
"uri": "lb://cloud-nacos-user-web",
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}
]
}
接下来,围绕这Route配置进行展开,详细说说url、predicate以及filter。
url
在实际开发过程中,url配置方式有多种,不同的配置前缀,如 lb://代表会走SpringCloud内置的负载均衡器,这也是我们项目主要用到的配置方式,实际上还有如下一些前缀,这些前缀实际上就是通过内置的全局GlobalFilter来匹配执行的,具体可以看后面的全局过滤器globalFilter那张图。
Predicate(断言):
断言是根据请求的路由信息,可以是url、请求参数,来判断当前的请求是否符合当前路由定义,虽然种类比较多,但我们实际生产环境中主要还是根据Path来匹配到网关后的微服务,Spring Cloud SCG内置了许多Predict,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中不过大家还是可以了解一下其它相关的断言定义。
Filter(过滤器):
过滤器主要有globalFilter和gatewayFilter,故名思意,globalFilter是全局过滤器,可以作用所有的路由,而gatewayFilter是针对具体的路由请求,开发人员可以根据自己的需求来决定实现那种filter,从定义上来看,两者都是接口。
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
public interface GatewayFilter extends ShortcutConfigurable {
String NAME_KEY = "name";
String VALUE_KEY = "value";
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
那么,贴心的开发人员也同样给我们提供了多种内置的过滤器。 内置局部路由过滤器gatewayFilter:
这里再另外抛几个小问题:
- 如果自定义的过滤器未定义Order,有默认顺序吗?
- GlobalFilter和GatewayFilter谁的优先级高,怎么编排的?
用下面测试示例来进行验证一下下:
@Slf4j
@Configuration
public class FilterConfig {
/**
* 全局过滤器-A
*
* @return
*/
@Bean
public GlobalFilter aGlobalFilter() {
return new MyGlobalFilter("全局过滤器-A", 1);
}
/**
* 全局过滤器-B
*
* @return
*/
@Bean
public GlobalFilter bGlobalFilter() {
return new MyGlobalFilter("全局过滤器-B", 2);
}
/**
* 自定义用户服务路由过滤器-A
*
* @return
*/
@Bean
public GatewayFilterFactory<MyGatewayFilter> myUserRouteAGatewayFilterFactory() {
return new MyUserRouteAGatewayFilterFactory("自定义用户服务路由过滤器-A", 3);
}
/**
* 自定义用户服务路由过滤器-B
*
* @return
*/
@Bean
public GatewayFilterFactory<MyGatewayFilter> myUserRouteBGatewayFilterFactory() {
return new MyUserRouteBGatewayFilterFactory("自定义用户服务路由过滤器-B", 4);
}
/**
* 默认过滤器A
*
* @return
*/
@Bean
public GatewayFilterFactory<MyGatewayFilter> myDefaultAGatewayFilterFactory() {
return new MyDefaultAGatewayFilterFactory("默认过滤器-A", 5);
}
/**
* 默认过滤器B
*
* @return
*/
@Bean
public GatewayFilterFactory<MyGatewayFilter> myDefaultBGatewayFilterFactory() {
return new MyDefaultBGatewayFilterFactory("默认过滤器-B", 6);
}
}
具体是这么配置的,application.yaml中(还有其它配置方式,同学们可以根据自己的来):
spring:
cloud:
gateway:
default-filters:
- name: MyDefaultA #默认过滤器名
- name: MyDefaultB
在nacos配置文件中,新增两单路由过滤配置:
{
"id": "user-router",
"order": 0,
"predicates": [
{
"args": {
"pattern": "/user-service/**"
},
"name": "Path"
}
],
"uri": "lb://user-service",
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
},
{
"name": "MyUserRouteA", # 针对用户服务的路由过滤A
"args": {}
},{
"name": "MyUserRouteB", # 针对用户服务的路由过滤B
"args": {}
}
]
}
请求并看一下网关拦截的打印情况:
c.ddoubuy.gateway.config.MyGlobalFilter : 全局过滤器-A, 优先级: 1 pre
c.ddoubuy.gateway.config.MyGlobalFilter : 全局过滤器-B, 优先级: 2 pre
c.d.gateway.config.MyGatewayFilter : 自定义用户服务路由过滤器-A, 优先级: 3 pre
c.d.gateway.config.MyGatewayFilter : 自定义用户服务路由过滤器-B, 优先级: 4 pre
c.d.gateway.config.MyGatewayFilter : 默认过滤器-A, 优先级: 5 pre
c.d.gateway.config.MyGatewayFilter : 默认过滤器-B, 优先级: 6 pre
com.alibaba.nacos.client.naming : new ips(1) service: DEFAULT_GROUP@@user-service -> [{"instanceId":"192.168.1.7#8001#DEFAULT#DEFAULT_GROUP@@user-service","ip":"192.168.1.7","port":8001,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@user-service","metadata":{"preserved.register.source":"SPRING_CLOUD"},"ipDeleteTimeout":30000,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000}]
com.alibaba.nacos.client.naming : current ips:(1) service: DEFAULT_GROUP@@user-service -> [{"instanceId":"192.168.1.7#8001#DEFAULT#DEFAULT_GROUP@@user-service","ip":"192.168.1.7","port":8001,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@user-service","metadata":{"preserved.register.source":"SPRING_CLOUD"},"ipDeleteTimeout":30000,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000}]
c.d.gateway.config.MyGatewayFilter : 默认过滤器-B, 优先级: 6 post
c.d.gateway.config.MyGatewayFilter : 默认过滤器-A, 优先级: 5 post
c.d.gateway.config.MyGatewayFilter : 自定义用户服务路由过滤器-B, 优先级: 4 post
c.d.gateway.config.MyGatewayFilter : 自定义用户服务路由过滤器-A, 优先级: 3 post
c.ddoubuy.gateway.config.MyGlobalFilter : 全局过滤器-B, 优先级: 2 post
c.ddoubuy.gateway.config.MyGlobalFilter : 全局过滤器-A, 优先级: 1 post
可见,三大种过滤器没有明确的优先级关系,完全是根据定义的Order顺序来的,秘诀就在于 org.springframework.cloud.gateway.handler.FilteringWebHandler的handle方法,内部会将所有的过滤器进行排序:
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = (Route)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
// 获取当前路由的过滤器(globalFalter)包括default-filters和内置filters以及自定义filters
List<GatewayFilter> gatewayFilters = route.getFilters();
// 获取全局过滤器
List<GatewayFilter> combined = new ArrayList(this.globalFilters);
// 将当前路由过滤器追加到全局列表后,所以同样order的编号优先级一目了然喔
combined.addAll(gatewayFilters);
// 排序开整
AnnotationAwareOrderComparator.sort(combined);
return (new DefaultGatewayFilterChain(combined)).filter(exchange);
}
顺便断点看看,符合预期。
总结
之前在项目中,对网关进行了一些开发,但是很多概念和原理并没有深入研究和学习,以本篇为始,继续会继续分享项目中具体的网关应用与踩坑以及源码相关学习,欢迎大家一起学习。
参考:
我不是大佬,我只是在学习路上。