把SpringCloudGateway这些东西理一理,感觉我又飞升了

288 阅读7分钟

如果你现在所接触的项目是基于微服务架构,如果碰巧你们项目使用的是springcloud alibaba,再碰巧网关是用的Spring Cloud Gateway,那不妨停留片刻,了解一下这些概念挺好。

注:如果大家没意见,本文就用SCG简代替Spring Cloud Gateway了。

  1. 清楚为什么选择SCG?
  2. SCG的请求流程?
  3. SCG的核心组件有哪些?

1. 为什么选择SCG

As we known,SCG是SpringCloud生态中的网关标配,基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty,非阻塞模型,性能更好,下图是来自官网图

image.png 做了一个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,大家有兴趣也可以自己测试一下。

webflux和mvc对比图

2. 核心请求流程

image.png

注:来自官网,步骤说明:

  1. 客户端向网关Gateway发送请求;
  2. Gateway Handler Mapping阶段确定当前请求与路由匹配(通过断言predicate匹配到具体的路由配置),随后将其发送到Gateway web handler处理;
  3. 由 Gateway web handler 创建过滤器链并调用;
  4. 通过特定于请求的 Fliter 链运行请求,先执行所有“pre”过滤器逻辑;
  5. 进行代理请求,通过RPC调用后端的微服务;
  6. 执行“post”过滤器逻辑;
  7. 将处理完后的响应体封装返回给客户端。

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包中不过大家还是可以了解一下其它相关的断言定义。

image.png

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:

image.png

内置全局过滤器globalFilter:

1669027557278.png

注:针对我们自定义的过滤器,我们需要一般需要指定过滤器对应的Order来决定该过滤器执行的顺序,多个Filter可以通过@Order或者getOrder()方法指定每个GlobalFilter的执行顺序,order值越小,Filter执行的优先级越高。

这里再另外抛几个小问题:

  1. 如果自定义的过滤器未定义Order,有默认顺序吗?
  2. 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);
}

顺便断点看看,符合预期。 过滤器执行顺序

总结

之前在项目中,对网关进行了一些开发,但是很多概念和原理并没有深入研究和学习,以本篇为始,继续会继续分享项目中具体的网关应用与踩坑以及源码相关学习,欢迎大家一起学习。

参考:

  1. 部分漂亮图参考自这位技术博主

微信图片_20221031231321.jpg

我不是大佬,我只是在学习路上。

扫码_搜索联合传播样式-标准色版.png