Spring Cloud Gateway入坑记
前提最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理。之前,很多网关应用使用的是Spring-Cloud-Netfilx基于Zuul1.x版本实现的那套方案,但是鉴于Zuul1.x已经停止迭代,它使用的是比较传统的阻塞(B)IO + 多线程的实现方案,其实性能不太好。后来Spring团队干脆自己重新研发了一套网关组件,这个就是本次要调研的Spring
前提
最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理。之前,很多网关应用使用的是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-Data
和Spring-Security
)和同步编程模式在Spring Cloud Gateway
中并不适用,所以最好先阅读一下上面提到的三个框架的文档。
Spring Cloud Gateway
依赖于Spring Boot
和Spring WebFlux
提供的基于Netty
的运行时环境,它并非构建为一个WAR包或者运行在传统的Servlet
容器中。
专有名词
- 路由(Route):路由是网关的基本组件。它由ID,目标URI,谓词(Predicate)集合和过滤器集合定义。如果谓词聚合判断为真,则匹配路由。
- 谓词(Predicate):使用的是Java8中基于函数式编程引入的java.util.Predicate。使用谓词(聚合)判断的时候,输入的参数是
ServerWebExchange
类型,它允许开发者匹配来自HTTP请求的任意参数,例如HTTP请求头、HTTP请求参数等等。 - 过滤器(Filter):使用的是指定的
GatewayFilter
工厂所创建出来的GatewayFilter
实例,可以在发送请求到下游之前或者之后修改请求(参数)或者响应(参数)。
其实Filter
还包括了GlobalFilter
,不过在官方文档中没有提到。
工作原理
客户端向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.SR1
,Greenwich
可以在伦敦地铁站的地图查到这个站点,对应的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-WebFlux
的HandlerMapping
组件基础设施的一部分,也就是HandlerMapping
进行匹配的时候,会把配置好的路由规则也纳入匹配机制之中。Spring Cloud Gateway
自身包含了很多内建的路由谓词工厂。这些谓词分别匹配一个HTTP请求的不同属性。多个路由谓词工厂可以用and
的逻辑组合在一起。
目前Spring Cloud Gateway
提供的内置的路由谓词工厂如下:
指定日期时间规则路由谓词
按照配置的日期时间指定的路由谓词有三种可选规则:
- 匹配请求在指定日期时间之前。
- 匹配请求在指定日期时间之后。
- 匹配请求在指定日期时间之间。
值得注意的是,配置的日期时间必须满足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
网关路由配置:
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:Bar
,application.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过滤器链的底层。示意图如下:
例如要实现负载均衡的功能,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是目前网络编程框架的主流方向,最好要跟上主流的步伐掌握这些框架的使用,有能力最好成为它们的贡献者。
目前常见的反应式编程框架有:
- Reactor和
RxJava2
,其中Reactor
在后端的JVM应用比较常见,RxJava2
在安卓编写的APP客户端比较常见。 Reactor-Netty
,这个是基于Reactor
和Netty
封装的。Spring-WebFlux
和Spring-Cloud-Gateway
,其中Spring-Cloud-Gateway
依赖Spring-WebFlux
,而Spring-WebFlux
底层依赖于Reactor-Netty
。
根据这个链式关系,最好系统学习一下Reactor
和Netty
。
参考资料:
附录
选用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 |
原文链接
- Github Page:http://www.throwable.club/2019/05/04/spring-cloud-gateway-guide
- Coding Page:http://throwable.coding.me/2019/05/04/spring-cloud-gateway-guide
(本文完 c-3-d e-a-20190504)
更多推荐
所有评论(0)