一文学透微服务网关 Spring Clud Gateway 的用法

2022-11-22 13:41:42 浏览数 (1)

前言

微服务网关在微服务项目中作为一个必不可少的组件,它在大型分布式微服务项目中可以起到路由转发、统一鉴权、请求日志记录、熔断降级和分布式限流等一些列的重要作用。因此,大部分微服务项目中都会有网关组件。Spring生态常用的微服务网关组件有 Spring Cloud Zuul 和 Spring Cloud Gateway。 前者是 奈飞公司开发的一个网关产品,属于Spring Cloud Netflix 中的一个组件,目前已停止维护,且对所有的Web请求是同步阻塞的。而 Spring Cloud Gateway 则是 Spring Cloud 团队自己开发的一套网关产品,属于 Spring Cloud 家族中的成员,可与 Spring Cloud 框架无缝集成,且 Spring Cloud Gateway 对所有的 Web 请求都是异步非阻塞的,性能相比 Zuul 更优。

Spring Cloud 2.x 实现了Spring Cloud社区生态下的Spring Cloud Gateway (简称SGC) 微服务网关项目。

Spring Cloud Gateway 基于WebFlux框架开发,目标是替换掉Zuul。本文我们就来系统学习一番 Spring Cloud Gateway 微服务网关的用法,笔者力争通过这篇文章带领大家学透 Spring Cloud Gateway 的用法。

Spring Cloud Gateway 概述

Spring Cloud Gateway 主要有两个特征:

  • 默认使用RxNetty作为相应式Web容器,通过非阻塞方式,利用较少的线程和资源来处理高并发请求,并提升服务资源利用的可伸缩性
  • 函数式编程端点, 通过使用Spring Web Flux 的函数式编程模式定义路由端点,处理请求

Spring Cloud Gateway 可于与Eureka、Rebon、Hystrix等组件配合使用,基于Spring5的Reactor框架和Spring Boot 2构建,使用Neytty作为底层通信框架,支持异步非阻塞编程模型和响应式编程框架,解决了Zuul框架的I/O阻塞问题。使用Spring WebFlux可以使 Spring Cloud Gateway在高并发场景下具有更好的性能表现,占用更少的资源。下面是Spring Cloud官方对Spring Cloud Gateway特征的介绍。

  • 基于Spring Framework 5, Reactor 和 Spring Boot 2.0框架
  • 根据请求的属性可以匹配对应的路由
  • 集成Hystrix
  • 集成Spring Cloud DiscoveryClient
  • 把易于编写的的Predicates和Filters作用于特定路由
  • 具备一些网关的高级功能,如动态路由、限流、路径重写

对于微服务网关来说,最核心的特征包括路由和过滤器机制。从功能特性上来看,Spring Cloud Gateway和Zuul具有相似的特性。它们都可以集成Hystrix、Ribon负载均衡以及Spring Cloud 的现有组件来实现附加功能。而Spring Cloud Gateway的本质特征还体现在底层的通信框架上,它可以基于Netty的多路复用和事件响应机制来实现网络通信;它的另外一大特性就是使用Spring 5 的响应式编程模型,允许通过Spring WebFlux实现异步非阻塞问题,在性能和资源的利用率上,都有了质的提升。在编程范式上,Spring Cloud Gateway使用函数式编程模式。官方提供的Spring Cloud Gateway的架构图如下所示:

图 1 Spring Cloud Gagteway 架构图

外部请求首先会经过Spring Cloud Gateway网关的处理器映射器,然后到达网关的Web处理器,接下来再经过一些列的过滤器,最后才到达代理服务。

Spring Cloud Gateway 核心概念

简单说明一下架构图中的三个术语

  • Filter(过滤器):和Zuul的过滤器在概念上类似,可以使用Filter拦截和修改请求,实现对上游的响应,进行二次处理,实现横切与应用无关的的功能,如安全、访问超时设置、限流等功能。
  • Route(路由):网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个ID、一个目标URI、一组断言和一组过滤器组成。如果断言为真,则路由匹配,目标URI会被访问。
  • Predicate(断言):Predicate来自Java 8的接口,它可以用来匹配来自Http请求的任何内容,例如headers或参数。接口包含多种默认方法,并将Predicate组合程复杂的逻辑(与、或、非),可以用于接口参数校验、路由转发判断等

Spring Cloud Gateway的接入和配置

Spring Cloud Gateway依赖Spring WebFlux提供的Netty运行时环境,所以Spring Boot必须是2.0以上版本。基本的Spring Cloud 环境配置确认以后,主要接入步骤如下。

Maven依赖引入

代码语言:javascript复制
  <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

路由配置方式之配置文件方式

代码语言:javascript复制
eureka:
   instance:
      prefer-ip-address: true
   client:
      service-url:
        defaultZone: http://localhost:8888/eureka/
      server:
        port: 8080
spring:
   application:
      name: scg-api-gateway
   cloud:
      gateway:
         routes:
         - id: url-proxy-1
           uri: https://localhost:8010
           predicates: 
           - Path=/csdn
         - id: message-provider-route 
           uri: lb://message-provider
           predicates:
           - Path=/message-provider/**  

各字段含义如下:

  • id: 自定义的路由ID,保持唯一
  • uri: 目标服务地址
  • predicates: 路由条件,Predicates接受一个输入参数,返回一个布尔值结果。
    • 第一个Predicate基于URL的方式。配置文件的第一个路由的配置采用URL的方式,配置了一个ID为uri-proxy-1的URI代理规则。路由的规则为:当访问地址为http://localhost:8080/dsdn/1.jpg时会路由到上游的地址http://localhost:8010/1.jpg。
    • 第二个Predicate基于服务ID发现的方式。配置文件的第二个路由的配置采用与注册中心相结合的服务发现方式,与单个URI的路由配置相比,区别其实很小,仅在于URI的schema协议不同。单个URI的schema协议,一般为http或者https协议。

基于代码DSL方式的路由配置接入

路由转发功能同样可以通过代码来实现,我们可以在启动类GatewayApplication中添加customRoutelocator方法来定制转发规则,代码如下:

代码语言:javascript复制
  @SpringBootApplication
public class GatewayApplication {
    public static void main(Stirng[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
    
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes().route("path_route", r->r.path("/csdn").uri("https://localhost:8010")).build();
    }
}

Spring Cloud Gateway 工作原理

客户端向Spring Cloud Gateway 发出http请求后,如果Gateway HandlerMapping 确定请求与路由匹配, 则将其发送到Gateway WebHandler。 WebHandler通过该请求的特定过滤器链处理请求。过滤器请求可以在发送代理请求之前后之后执行逻辑。在Spring Cloud Gateway的执行流程中,首先执行所有的"Pre filter"逻辑,然后执行回源请求代理,在请求代理执行完后,执行"post filter"逻辑。在"pre"类型的过滤器中,可以实现参数校验、权限校验、流量监控、日志输出、协议转换等功能;在"post"类型过滤器中,可以实现响应内容、响应头的修改,日志的输出、流量监控等功能。核心工作流程如下图所示:

图 2 Spring Cloud Gateway 工作流程图

Predicate 条件

在Spring Cloud Gateway 中, Spring 利用Predicate的特性实现了各种路由匹配规则,通过Header、请求参数等不同条件来匹配对应的路由。

我们来看Spring Cloud Gateway 内置的集中使用 Predicate 的方法。

代码语言:javascript复制
server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
      - id: gateway-service
        uri: https://localhost:8010
        order: 0
        predicates:
           - Host=**.foo.org
           - Path=/headers
           - Method=GET
           - Header=X-Request-Id, d 
           - Query=foo, ba.
           - Cookie=chocolate, ch.p
           - After=2020-08-20T17:42:47.789-07:00[America/Denver]
           - Before=2020-08-2017:42:47.789[America/Denver]

在上述配置文件中,如果多种 Predicate 同时存在于同一个路由,请求必须满足所有条件才能被这个路由匹配。

当一个请求满足多个 Predicate 条件时,请求只会被首个成功匹配的路由转发。下面分别对不同规则的路由进行解释。

  • 通过请求路径匹配(Path Route Predicate) 路由断言工厂接收一个参数,根据 Path 定义好规则来判断访问的URI 是否匹配。配置示例如下:
代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
       - id: host_route
         uri: https://localhost:8010
         predicates:
          - Path=/hello/{segment}

如果请求路径符合要求,则此路由将匹配,例如 /hello/1 或者 /hello/world

使用 curl 测试,命令行输入:

代码语言:javascript复制
curl http://localhost:8080/hello/1
curl http://localhost:8080/hello/xx
curl http://localhost:8080/boo/xx

经过测试发现,第一条和第二条命令可以正常获取页面返回值,最后一条命令报 404 错误,证明路由是通过制定路径来匹配的。

  • 通过请求参数匹配 (Query Route Predicate) 路由断言工厂接收两个参数:一个必需的参数和一个可选的正则表达式。配置示例如下:
代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://localhost:8010
        predicates:
        - Query=helloworld

在这样的配置中,只有请求中包含hellowold属性的参数即可匹配路由,不带helloworld参数则不会匹配。还可将 Query 的值以键值对的方式进行配置,这样在请求时会对属性值和正则表达式都进行匹配,键值对匹配后才会进行执行路由逻辑。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: localhost:8010
        predicates:
        - Query=hello, world

在上述路由匹配中,请求中包含 hello 属性并且参数值是以 world 开头、长度为三位的字符串,才会进行匹配和路由。使用 curl 测试,命令行输入:

代码语言:javascript复制
curl localhost:8080?hello=world

测试可以返回正确的页面代码。如果将 hello 的属性值改为 ok, 再次访问就会报 404 错误,证明路由需要匹配正则表达式才会进行路由。

  • 通过请求方法匹配

路由断言工厂接收一个参数,即需要匹配 HTTP 方法。通过 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      -id: method_route
       uri: http://localhost:8010
       predicates:
       - Method=GET

使用 curl 测试(# curl 默认以 GET 的方式请求),命令行输入:

curl http://localhost:8080

测试返回页面代码,证明匹配到路由。

我们再以 POST 的方式请求测试。

curl -X POST http://localhost:8080

返回 404 错误表示没有找到,证明没有匹配上路由。

  • 通过 Header 属性匹配 路由断言工厂接收两个参数,分别式请求头名称和正则表达式。Header Route Predicate 和 Cookie Route Predicate 一样,也是接收两个参数:一个 header 的属性值和一个正则表达式。这个属性值和正则表达式匹配则执行。
代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: localhost:8010
        predicates:
        - Header=X-Request-ID, d 

通过 Host 路由匹配

Spring Cloud Gateway 可以根据 Host 名进行匹配转发, Host Route Predicate 接收一组参数,一组匹配的域名列表。它通过参数中的主机地址作为匹配规则。

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://xxx.com
        predicates:
        - Host=**.xxx.com

使用 curl 测试, 输入命令行

代码语言:javascript复制
curl http://localhost:8080 -H "Host: www.xxx.com"
curl http://localhost:8080 -H "Host: hello.xxx.com"

通过测试以上两种 Host 设置方式,均可匹配到 host_route, 去掉 host 参数则会报 404 错误。

  • 时间匹配

Predicate 支持设置时间,在请求转发时,先判断这个时间与我们设置的时间,然后进行转发。又分为设置时间后断言、设置时间前断言和设置时间之间断言。

设置时间后断言:从 After Route Predicate Factory 中获取一个UTC 的时间格式参数,当请求的当前时间在配置的 UTC 时间之后,则匹配成功,否则匹配失败。下面时配置示例:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
       - id: time_route
         uri: localhost:8010
         predicates:
         - After=2018-01-20T06:06:06 08:00[Asia/Shanghai]

设置时间前断言:从 Before Route Predicate Factory 中获取一个 UTC 时间格式参数,当请求的当前时间在配置的 UTC 时间之前,则匹配成功,否则匹配失败。下面是配置示例:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
       - id: after_route
         uri: localhost:8010
         predicates: 
         - Before=2018-01-20T06:06:06 08:00[Asia/Shanghai]

设置时间之间断言:从 Between Route Predicate Factory 中获取一个 UTC 时间格式的参数,当请求的当前时间在配置的 UTC 之间, 则匹配成功,否则匹配失败。下面是配置示例:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
       - id: after_route
         uri: localhost:8010
         predicates:
         - Between=2018-01-20T06:06:06[Asia/Shanghai]
             2019-01-20T06:06:06[Asia/Shanghai]
  • 通过 Cookie 匹配

Cookie 路由断言会会取两个参数, 一个是 Cookie name, 一个是正则表达式,路由规则是通过获取对应的Cookie name 值和正则表达式进行匹配,如果匹配上就会执行路由,否则不执行路由。下面是配置示例:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
       - id: cookie_route
         uri: http://localhost:8050/test/cookie
         predicates:
         - Cookie=hello, test
  • 通过 IP 地址匹配

Remote Addr Route Predicate Factory 配置一个 IPv4 或者 IPv6 网络的字符串或 IP 地址。当请求的 IP 地址在网段之内或者配置的 IP 地址相同,匹配上则进行转发,否则不进行转发。下面是配置示例:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: localhost:8010
        predicates:
        - RemoteAddr=192.168.1.1/50

可以将 curl localhost:8080 设置为本机的 IP 地址进行测试,如果请求的远程地址在192.168.1.1~192.168.1.50区间,则路由匹配上进行请求转发,否则不会转发。

GatewayFilter 与 GlobalFilter

Spring Cloud Gateway 中有两种 Filter, 一种是 GlobalFilter (全局过滤器),一种是GatewayFilter。GlobalFilter 默认对所有的路由有效,GatewayFilter 需要通过路由分组指定。GlobalFilter 接口与 GatewayFilter 接口具有相同的签名,是有条件地应用于所有路由的特殊过滤器。

当请求进入路由匹配逻辑时,Web Handler 会将 GlobalFilter 的所有实例 和 GatewayFilter 路由特定实例添加到 Filter Chain 组件。Filter 组合执行的顺序由 Order 接口决定,可以通过 getOrder 方法 或使用 @Order 注解来设置。Spring Cloud Gateway 通过执行过滤器将逻辑分为“前置”和“后置” 阶段, 优先级较高的前置过滤器会优先被执行,而优先级较高的后置过滤器的执行顺序正好相反,最后执行。

GatewayFilter Factories

过滤器允许以某种方式修改传入的 Http 请求或返回 Http 响应。过滤器的作用是某些特定路由。Spring Cloud Gateway 包括许多内置的过滤器工厂。

  • 实现前缀修改(增加前缀、去掉前缀)

PrefixPathGatewayFilterFactoryStriPrefixGatewayFilterFactory 是一对处理请求URL 的前缀 和 Filter 工厂,前者添加前缀,后者去除前缀。

配置文件 application.yml 如下:

代码语言:javascript复制
spring:
  cloud:
    gateway:
    #配置所有路由的默认过滤器,这里配置的是gatewayFilter
      default-filter:
       routes:
        - id: filter_route
          uri: lb://server-test #服务的application名称
          order: 0 #路由级别
          predicates:
          - Path=/bus/**
          filters:
           - prefixPath=/mypath
           - StripPrefix=2 #去掉前缀,去几层

PrefixPathGatewayFilterFactory 允许我们对相应的路由请求前增加前缀。例如实例配置中的请求/hello, 最后转发到目标服务的路径变为/mypath/hello。

StripPrefixGatewayFilterFactory 允许我们将对应的路由请求去除前缀,例如实例配置中的请求name/bar/foo, 去掉前面两个前缀后,最后转发到目标服务的路径为/foo。

  • 实现请求头内容添加和改写

AddRequestHeaderGatewayFactory 采用一对名称和值作为参数,配置文件application.yml 如下:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: add_requerst_header_route
        uri: http://localhost:8010
        filters:
        - AddRequestHeader=X-Request-Foo, Bar

对于所有匹配的请求,将在向下游的请求头内容中添加x-request-foo:bar header

  • 实现请求体内容添加和改写

AddRequestParameterGatewayFilterFactory 采用一对名称和值作为参数,配置参数在application.yml文件中如下:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_parameter_route
        uri: localhost:8010
        filters:
        - AddRequestParameter=foo, bar

对于所有匹配的请求,将向下游请求添加foo=bar查询字符串。

  • 实现熔断降级 HystrixGatewayFilter允许向网关路由引入Hystrix, 保护服务不受级联故障影响,并允许在下游故障时提供fallback响应。要在项目中启用Hystrix网关过滤器,需要向Hystrix的依赖HystrixGatewayFilterFactory 添加一个name参数,即HystrixCommand 的名称,配置文件application.yml如下:
代码语言:javascript复制
spring:
  cloud:
    routes:
    - id: hystrix_route
      uri: lb://test-service:8080
      filters:
      - name: Hystrix
        args: 
          name: fallbackcmd
          fallbackUri: forword:/incaseoffailureusethis
      - RewritePath=/comsumingserviceendpoint, /backingserviceendpoint

当调用hystrix fallback 时, 将转发到/incaseoffailureusethis。注意,这个示例还演示了通过目标URI上的"lb"前缀将 Spring Cloud Netflix Ribbon 客户端实现负载均衡。主要场景是网关应用程序中的内部控制器或处理程序使用 fallbackUri,它也可以将请求重新路由到外部应用程序中的控制器或处理程序。

  • 分布式限流 Spring Cloud Gateway 内置的RequestRateLimiterGatewayFilterFactory 提供限流能力,基于令牌桶算法实现。目前它内置的 RedisRateLimiter, 依赖 Redis 来存储限流配置和统计数据。我们也可以实现自己的RateLimiter。 只需实现 Spring Cloud Gateway 自带的 RateLimiter 接口或者継承 AbstractRateLimiter>

首先,添加Maven 依赖:

代码语言:javascript复制
  <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>

其次,添加限流配置:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: lb://user-center
        predicates:
          - TimeBetween=上午0:00, 下午11:59
        filters:
          - AddRequestHeader=X-Request-Foo, Bar
          - name: RequestRateLimiter
            args:
              #令牌桶每秒填充的平均速率
              redis-rate-limiter.replenishRate: 1
              #令牌桶的上限
              redis-rate-limiter.burstCapacity: 2
              #使用 SpEL表达式从Spring 容器中获取 Bean 对象
              key-resolver: "#{@pathKeyResolver}"
 redis:
   host: 127.0.0.1
   port: 6379

最后,完成对 Path 的 KeyResolver (可以通过 KeyResolver 来指定限流的 Key), 实现对特定 Path 下的限流控制配置。在过滤器中可以配置一个可选的 KeyResolver, KeyResolver 在配置中根据名称使用 SpEL 引用 bean。#{@pathKeyResolver} 是引用名为 “pathKeyResolver"的 Bean 的 SpEL 表达式。KeyResolver 接口允许可插拔策略来派生限制请求的 Key。 代码示例如下:

代码语言:javascript复制
@Configuration
public class Configuration {
    
    /**
    * 按照 Path 限流
    * @return key
    */
    @Bean
    public KeyResolver pathKeyResolver(){
        return exchange -> Mono.just(
           exchange.getRequest()
              .getPath()
              .toString()
        );
    }
}

参考阅读

【1】Spring Cloud Gateway(https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-prefixpath-gatewayfilter-factory)

【2】王佩华著《微服务架构深度解析原理、实践与进阶》之10.4 Spring Cloud Gateway

0 人点赞