前提

最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理。之前,很多网关应用使用的是Spring-Cloud-Netfilx基于Zuul1.x版本实现的那套方案,但是鉴于Zuul1.x已经停止迭代,它使用的是比较传统的阻塞(B)IO + 多线程的实现方案,其实性能不太好。后来Spring团队干脆自己重新研发了一套网关组件,这个就是本次要调研的Spring-Cloud-Gateway

简介

Spring Cloud Gateway依赖于Spring Boot 2.0, Spring WebFlux,和Project Reactor。许多熟悉的同步类库(例如Spring-DataSpring-Security)和同步编程模式在Spring Cloud Gateway中并不适用,所以最好先阅读一下上面提到的三个框架的文档。

Spring Cloud Gateway依赖于Spring BootSpring WebFlux提供的基于Netty的运行时环境,它并非构建为一个WAR包或者运行在传统的Servlet容器中。

专有名词

  • 路由(Route):路由是网关的基本组件。它由ID,目标URI,谓词(Predicate)集合和过滤器集合定义。如果谓词聚合判断为真,则匹配路由。
  • 谓词(Predicate):使用的是Java8中基于函数式编程引入的java.util.Predicate。使用谓词(聚合)判断的时候,输入的参数是ServerWebExchange类型,它允许开发者匹配来自HTTP请求的任意参数,例如HTTP请求头、HTTP请求参数等等。
  • 过滤器(Filter):使用的是指定的GatewayFilter工厂所创建出来的GatewayFilter实例,可以在发送请求到下游之前或者之后修改请求(参数)或者响应(参数)。

其实Filter还包括了GlobalFilter,不过在官方文档中没有提到。

工作原理

s-c-g-e-1.png

客户端向Spring Cloud Gateway发出请求,如果Gateway Handler Mapping模块处理当前请求如果匹配到一个目标路由配置,该请求就会转发到Gateway Web Handler模块。Gateway Web Handler模块在发送请求的时候,会把该请求通过一个匹配于该请求的过滤器链。上图中过滤器被虚线分隔的原因是:过滤器的处理逻辑可以在代理请求发送之前或者之后执行。所有pre类型的过滤器执行之后,代理请求才会创建(和发送),当代理请求创建(和发送)完成之后,所有的post类型的过滤器才会执行。

见上图,外部请求进来后如果落入过滤器链,那么虚线左边的就是pre类型的过滤器,请求先经过pre类型的过滤器,再发送到目标被代理的服务。目标被代理的服务响应请求,响应会再次经过滤器链,也就是走虚线右侧的过滤器链,这些过滤器就是post类型的过滤器。

注意,如果在路由配置中没有明确指定对应的路由端口,那么会使用如下的默认端口:

  • HTTP协议,使用80端口。
  • HTTPS协议,使用443端口。

引入依赖

建议直接通过Train版本(其实笔者考究过,Train版本的代号其实是伦敦地铁站的命名,像当前的Spring Cloud最新版本是Greenwich.SR1Greenwich可以在伦敦地铁站的地图查到这个站点,对应的SpringBoot版本是2.1.x)引入Spring-Cloud-Gateway,因为这样可以跟上最新稳定版本的Spring-Cloud版本,另外由于Spring-Cloud-Gateway基于Netty的运行时环境启动,不需要引入带Servlet容器的spring-boot-starter-web

父POM引入下面的配置:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

子模块或者需要引入Spring-Cloud-Gateway的模块POM引入下面的配置:

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>

创建一个启动类即可:

@SpringBootApplication
public class RouteServerApplication {

public static void main(String[] args) {
SpringApplication.run(RouteServerApplication.class, args);
}
}

网关配置

网关配置最终需要转化为一个RouteDefinition的集合,配置的定义接口如下:

public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}

通过YAML文件配置或者流式编程式配置(其实文档中还有配合Eureka的DiscoveryClient进行配置,这里暂时不研究),最终都是为了创建一个RouteDefinition的集合。

Yaml配置

配置实现是PropertiesRouteDefinitionLocator,关联着配置类GatewayProperties

spring:
cloud:
gateway:
routes:
- id: datetime_after_route # <------ 这里是路由配置的ID
uri: http://www.throwable.club # <------ 这里是路由最终目标Server的URI(Host)
predicates: # <------ 谓词集合配置,多个是用and逻辑连接
- Path=/blog # <------- Key(name)=Expression,键是谓词规则工厂的ID,值一般是匹配规则的正则表示

编程式流式配置

编程式和流式编程配置需要依赖RouteLocatorBuilder,目标是构造一个RouteLocator实例:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/blog")
.uri("http://www.throwable.club")
)
.build();
}

路由谓词工厂

Spring Cloud Gateway将路由(Route)作为Spring-WebFluxHandlerMapping组件基础设施的一部分,也就是HandlerMapping进行匹配的时候,会把配置好的路由规则也纳入匹配机制之中。Spring Cloud Gateway自身包含了很多内建的路由谓词工厂。这些谓词分别匹配一个HTTP请求的不同属性。多个路由谓词工厂可以用and的逻辑组合在一起。

目前Spring Cloud Gateway提供的内置的路由谓词工厂如下:

s-c-g-e-2.png

指定日期时间规则路由谓词

按照配置的日期时间指定的路由谓词有三种可选规则:

  • 匹配请求在指定日期时间之前。
  • 匹配请求在指定日期时间之后。
  • 匹配请求在指定日期时间之间。

值得注意的是,配置的日期时间必须满足ZonedDateTime的格式:

//年月日和时分秒用'T'分隔,接着-07:00是和UTC相差的时间,最后的[America/Denver]是所在的时间地区
2017-01-20T17:42:47.789-07:00[America/Denver]

例如网关的应用是2019-05-01T00:00:00+08:00[Asia/Shanghai]上线的,上线之后的请求都路由奥www.throwable.club,那么配置如下:

server 
port: 9090
spring:
cloud:
gateway:
routes:
- id: datetime_after_route
uri: http://www.throwable.club
predicates:
- After=2019-05-01T00:00:00+08:00[Asia/Shanghai]

此时,只要请求网关http://localhost:9090,请求就会转发到http://www.throwable.club

如果想要只允许2019-05-01T00:00:00+08:00[Asia/Shanghai]之前的请求,那么只需要改为:

server 
port: 9091
spring:
cloud:
gateway:
routes:
- id: datetime_before_route
uri: http://www.throwable.club
predicates:
- Before=2019-05-01T00:00:00+08:00[Asia/Shanghai]

如果只允许两个日期时间段之间的时间进行请求,那么只需要改为:

server 
port: 9090
spring:
cloud:
gateway:
routes:
- id: datetime_between_route
uri: http://www.throwable.club
predicates:
- Between=2019-05-01T00:00:00+08:00[Asia/Shanghai],2019-05-02T00:00:00+08:00[Asia/Shanghai]

那么只有2019年5月1日0时到5月2日0时的请求才能正常路由。

Cookie路由谓词

CookieRoutePredicateFactory需要提供两个参数,分别是Cookie的name和一个正则表达式(value)。只有在请求中的Cookie对应的name和value和Cookie路由谓词中配置的值匹配的时候,才能匹配命中进行路由。

server 
port: 9090
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://www.throwable.club
predicates:
- Cookie=doge,throwable

请求需要携带一个Cookie,name为doge,value需要匹配正则表达式”throwable”才能路由到http://www.throwable.club

这里尝试本地搭建一个订单Order服务,基于SpringBoot2.1.4搭建,启动在9091端口:

// 入口类
@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}

@GetMapping(value = "/cookie")
public ResponseEntity<String> cookie(@CookieValue(name = "doge") String doge) {
return ResponseEntity.ok(doge);
}
}

订单服务application.yaml配置:

spring:
application:
name: order-service
server:
port: 9091
```

网关路由配置:

```yaml
spring:
application:
name: route-server
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:9091
predicates:
- Cookie=doge,throwable
curl http://localhost:9090/order/cookie --cookie "doge=throwable"

//响应结果
throwable

Header路由谓词

HeaderRoutePredicateFactory需要提供两个参数,分别是Header的name和一个正则表达式(value)。只有在请求中的Header对应的name和value和Header路由谓词中配置的值匹配的时候,才能匹配命中进行路由。

订单服务中新增一个/header端点:

@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}

@GetMapping(value = "/header")
public ResponseEntity<String> header(@RequestHeader(name = "accessToken") String accessToken) {
return ResponseEntity.ok(accessToken);
}
}

网关的路由配置如下:

spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://localhost:9091
predicates:
- Header=accessToken,Doge
curl -H "accessToken:Doge" http://localhost:9090/order/header

//响应结果
Doge

Host路由谓词

HostRoutePredicateFactory只需要指定一个主机名列表,列表中的每个元素支持Ant命名样式,使用.作为分隔符,多个元素之间使用,区分。Host路由谓词实际上针对的是HTTP请求头中的Host属性。

订单服务中新增一个/header端点:

@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}

@GetMapping(value = "/host")
public ResponseEntity<String> host(@RequestHeader(name = "Host") String host) {
return ResponseEntity.ok(host);
}
}

网关的路由配置如下:

spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://localhost:9091
predicates:
- Host=localhost:9090
curl http://localhost:9090/order/host

//响应结果
localhost:9091 # <--------- 这里要注意一下,路由到订单服务的时候,Host会被修改为localhost:9091

其实可以定制更多样化的Host匹配模式,甚至可以支持URI模板变量。

- Host=www.throwable.**,**.throwable.**

- Host={sub}.throwable.club

请求方法路由谓词

MethodRoutePredicateFactory只需要一个参数:要匹配的HTTP请求方法。

网关的路由配置如下:

spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://localhost:9091
predicates:
- Method=GET

这样配置,所有的进入到网关的GET方法的请求都会路由到http://localhost:9091

订单服务中新增一个/get端点:

@GetMapping(value = "/get")
public ResponseEntity<String> get() {
return ResponseEntity.ok("get");
}
curl http://localhost:9090/order/get

//响应结果
get

请求路径路由谓词

PathRoutePredicateFactory需要PathMatcher模式路径列表和一个可选的标志位参数matchOptionalTrailingSeparator。这个是最常用的一个路由谓词。

spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://localhost:9091
predicates:
- Path=/order/path
@GetMapping(value = "/path")
public ResponseEntity<String> path() {
return ResponseEntity.ok("path");
}
curl http://localhost:9090/order/path

//响应结果
path

此外,可以通过{segment}占位符配置路径如/foo/1/foo/bar/bar/baz,如果通过这种形式配置,在匹配命中进行路由的时候,会提取路径中对应的内容并且将键值对放在ServerWebExchange.getAttributes()集合中,KEY为ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE,这些提取出来的属性可以供GatewayFilter Factories使用。

请求查询参数路由谓词

QueryRoutePredicateFactory需要一个必须的请求查询参数(param的name)以及一个可选的正则表达式(regexp)。

spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://localhost:9091
predicates:
- Query=doge,throwabl.

这里配置的param就是doge,正则表达式是throwabl.

@GetMapping(value = "/query")
public ResponseEntity<String> query(@RequestParam("name") String doge) {
return ResponseEntity.ok(doge);
}
curl http://localhost:9090/order/query?doge=throwable

//响应结果
throwable

远程IP地址路由谓词

RemoteAddrRoutePredicateFactory匹配规则采用CIDR符号(IPv4或IPv6)字符串的列表(最小值为1),例如192.168.0.1/16(其中192.168.0.1是远程IP地址并且16是子网掩码)。

spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://localhost:9091
predicates:
- RemoteAddr=127.0.0.1
@GetMapping(value = "/remote")
public ResponseEntity<String> remote() {
return ResponseEntity.ok("remote");
}
curl http://localhost:9090/order/remote

//响应结果
remote

关于远程IP路由这一个路由谓词其实还有很多扩展手段,这里暂时不展开。

多个路由谓词组合

因为路由配置中的predicates属性其实是一个列表,可以直接添加多个路由规则:

spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://localhost:9091
predicates:
- RemoteAddr=xxxx
- Path=/yyyy
- Query=zzzz,aaaa

这些规则是用and逻辑组合的,例如上面的例子相当于:

request = ...
if(request.getRemoteAddr == 'xxxx' && request.getPath match '/yyyy' && request.getQuery('zzzz') match 'aaaa') {
return true;
}
return false;

GatewayFilter工厂

路由过滤器GatewayFilter允许修改进来的HTTP请求内容或者返回的HTTP响应内容。路由过滤器的作用域是一个具体的路由配置。Spring Cloud Gateway提供了丰富的内建的GatewayFilter工厂,可以按需选用。

因为GatewayFilter工厂类实在太多,笔者这里举个简单的例子。

如果我们想对某些请求附加特殊的HTTP请求头,可以选用AddRequestHeaderX-Request-Foo:Barapplication.yml如下:

spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-Foo,Bar

那么所有的从网关入口的HTTP请求都会添加一个特殊的HTTP请求头:X-Request-Foo:Bar

目前GatewayFilter工厂的内建实现如下:

ID 类名 类型 功能
StripPrefix StripPrefixGatewayFilterFactory pre 移除请求URL路径的第一部分,例如原始请求路径是/order/query,处理后是/query
SetStatus SetStatusGatewayFilterFactory post 设置请求响应的状态码,会从org.springframework.http.HttpStatus中解析
SetResponseHeader SetResponseHeaderGatewayFilterFactory post 设置(添加)请求响应的响应头
SetRequestHeader SetRequestHeaderGatewayFilterFactory pre 设置(添加)请求头
SetPath SetPathGatewayFilterFactory pre 设置(覆盖)请求路径
SecureHeader SecureHeadersGatewayFilterFactory pre 设置安全相关的请求头,见SecureHeadersProperties
SaveSession SaveSessionGatewayFilterFactory pre 保存WebSession
RewriteResponseHeader RewriteResponseHeaderGatewayFilterFactory post 重新响应头
RewritePath RewritePathGatewayFilterFactory pre 重写请求路径
Retry RetryGatewayFilterFactory pre 基于条件对请求进行重试
RequestSize RequestSizeGatewayFilterFactory pre 限制请求的大小,单位是byte,超过设定值返回413 Payload Too Large
RequestRateLimiter RequestRateLimiterGatewayFilterFactory pre 限流
RequestHeaderToRequestUri RequestHeaderToRequestUriGatewayFilterFactory pre 通过请求头的值改变请求URL
RemoveResponseHeader RemoveResponseHeaderGatewayFilterFactory post 移除配置的响应头
RemoveRequestHeader RemoveRequestHeaderGatewayFilterFactory pre 移除配置的请求头
RedirectTo RedirectToGatewayFilterFactory pre 重定向,需要指定HTTP状态码和重定向URL
PreserveHostHeader PreserveHostHeaderGatewayFilterFactory pre 设置请求携带的属性preserveHostHeader为true
PrefixPath PrefixPathGatewayFilterFactory pre 请求路径添加前置路径
Hystrix HystrixGatewayFilterFactory pre 整合Hystrix
FallbackHeaders FallbackHeadersGatewayFilterFactory pre Hystrix执行如果命中降级逻辑允许通过请求头携带异常明细信息
AddResponseHeader AddResponseHeaderGatewayFilterFactory post 添加响应头
AddRequestParameter AddRequestParameterGatewayFilterFactory pre 添加请求参数,仅仅限于URL的Query参数
AddRequestHeader AddRequestHeaderGatewayFilterFactory pre 添加请求头

GatewayFilter工厂使用的时候需要知道其ID以及配置方式,配置方式可以看对应工厂类的公有静态内部类XXXXConfig

GlobalFilter工厂

GlobalFilter的功能其实和GatewayFilter是相同的,只是GlobalFilter的作用域是所有的路由配置,而不是绑定在指定的路由配置上。多个GlobalFilter可以通过@Order或者getOrder()方法指定每个GlobalFilter的执行顺序,order值越小,GlobalFilter执行的优先级越高。

注意,由于过滤器有pre和post两种类型,pre类型过滤器如果order值越小,那么它就应该在pre过滤器链的顶层,post类型过滤器如果order值越小,那么它就应该在pre过滤器链的底层。示意图如下:

s-c-g-e-3.png

例如要实现负载均衡的功能,application.yml配置如下:

spring:
cloud:
gateway:
routes:
- id: myRoute
uri: lb://myservice # <-------- lb特殊标记会使用LoadBalancerClient搜索目标服务进行负载均衡
predicates:
- Path=/service/**

目前Spring Cloud Gateway提供的内建的GlobalFilter如下:

类名 功能
ForwardRoutingFilter 重定向
LoadBalancerClientFilter 负载均衡
NettyRoutingFilter Netty的HTTP客户端的路由
NettyWriteResponseFilter Netty响应进行写操作
RouteToRequestUrlFilter 基于路由配置更新URL
WebsocketRoutingFilter Websocket请求转发到下游

内建的GlobalFilter大多数和ServerWebExchangeUtils的属性相关,这里就不深入展开。

跨域配置

网关可以通过配置来控制全局的CORS行为。全局的CORS配置对应的类是CorsConfiguration,这个配置是一个URL模式的映射。例如application.yaml文件如下:

spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "https://docs.spring.io"
allowedMethods:
- GET

在上面的示例中,对于所有请求的路径,将允许来自docs.spring.io并且是GET方法的CORS请求。

Actuator端点相关

引入spring-boot-starter-actuator,需要做以下配置开启gateway监控端点:

management.endpoint.gateway.enabled=true 
management.endpoints.web.exposure.include=gateway

目前支持的端点列表:

ID 请求路径 HTTP方法 描述
globalfilters /actuator/gateway/globalfilters GET 展示路由配置中的GlobalFilter列表
routefilters /actuator/gateway/routefilters GET 展示绑定到对应路由配置的GatewayFilter列表
refresh /actuator/gateway/refresh POST 清空路由配置缓存
routes /actuator/gateway/routes GET 展示已经定义的路由配置列表
routes/{id} /actuator/gateway/routes/{id} GET 展示对应ID已经定义的路由配置
routes/{id} /actuator/gateway/routes/{id} POST 添加一个新的路由配置
routes/{id} /actuator/gateway/routes/{id} DELETE 删除指定ID的路由配置

其中/actuator/gateway/routes/{id}添加一个新的路由配置请求参数的格式如下:

{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"doge":"/throwable"}
}],
"filters": [],
"uri": "https://www.throwable.club",
"order": 0
}

小结

笔者虽然是一个底层的码畜,但是很久之前就向身边的朋友说:

反应式编程结合同步非阻塞IO或者异步非阻塞IO是目前网络编程框架的主流方向,最好要跟上主流的步伐掌握这些框架的使用,有能力最好成为它们的贡献者。

目前常见的反应式编程框架有:

  • ReactorRxJava2,其中Reactor在后端的JVM应用比较常见,RxJava2在安卓编写的APP客户端比较常见。
  • Reactor-Netty,这个是基于ReactorNetty封装的。
  • Spring-WebFluxSpring-Cloud-Gateway,其中Spring-Cloud-Gateway依赖Spring-WebFlux,而Spring-WebFlux底层依赖于Reactor-Netty

根据这个链式关系,最好系统学习一下ReactorNetty

参考资料:

附录

选用Spring-Cloud-Gateway不仅仅是为了使用新的技术,更重要的是它的性能有了不俗的提升,基准测试项目spring-cloud-gateway-bench的结果如下:

代理组件(Proxy) 平均交互延迟(Avg Latency) 平均每秒处理的请求数(Avg Requests/Sec)
Spring Cloud Gateway 6.61ms 32213.38
Linkered 7.62ms 28050.76
Zuul(1.x) 12.56ms 20800.13
None(直接调用) 2.09ms 116841.15

(本文完 c-3-d e-a-20190504)