Spring Cloud Gateway 作为 Spring Cloud框架的第二代网关,在功能上要比 Zuul更加的强大,性能也更好。随着 Spring Cloud的版本迭代,Spring Cloud官方有打算弃用 Zuul的意思。在笔者调用了 Spring Cloud Gateway的使用和功能上,Spring Cloud Gateway替换掉 Zuul的成本上是非常低的,几乎可以无缝切换。Spring Cloud Gateway几乎包含了 Zuul的所有功能。
一、网关定义
API 网关是一个反向路由,屏蔽内部细节,为调用者提供统一入口,接收所有调用者请求,通过路由机制转发到服务实例。API 网关是一组“过滤器 Filter”集合,可以实现一系列与核心业务无关的横切面功能,如安全认证、限流熔断、日志监控。
网关在系统中所处的位置:
二、Zuul 与 Spring Cloud Gateway 比较
优点 | 缺点 | |
---|---|---|
Gateway | 1、线程开销小2、使用轻量级 Netty 异步IO实现通信3、支持各种长连接,WebSocket4、Spring 官方推荐,重点支持,功能较 Zuul更丰富,支持限流监控等 | 1、源码复杂2、运维复杂3、目前资料、实践较少,出现问题排查难度大 |
Zuul | 1、编码模型简单2、开发调试运维简单 | 1、线程上下文切换开销2、线程数限制3、延迟堵塞耗尽线程链接资源 |
**Zuul 与 Gateway 压测结果:**休眠时间模仿后端请求时间,线程数 2000,请求时间 360秒=6分钟。配置情况:Gateway 默认配置,Zuul网关的 Tomcat 最大线程数为 400,hystrix 超时时间为 100000。
休眠时间 | 测试样本,单位=个Zuul/Gateway | 平均响应时间,单位=毫秒Zuul/Gateway | 99%响应时间,单位=毫秒 Zuul/Gateway | 错误次数,单位=个Zuul/Gateway | 错误比例Zuul/Gateway |
---|---|---|---|---|---|
休眠100ms | 294134/1059321 | 2026/546 | 6136/1774 | 104/0 | 0.04%/0% |
休眠300ms | 101194/399909 | 5595/1489 | 15056/1690 | 1114/0 | 1.10%/0% |
休眠600ms | 51732/201262 | 11768/2975 | 27217/3203 | 2476/0 | 4.79%/0% |
休眠1000ms | 31896/120956 | 19359/4914 | 46259/5115 | 3598/0 | 11.28%/0% |
**测试结果:**Gateway在高并发和后端服务响应慢的场景下比 Zuul的表现要好
三、Spring Cloud GateWay 架构图
客户端向 Spring Cloud Gateway发出请求。 在 Gateway Handler Mapping中找到请求相的匹配路由(这个时候就用到 predicate),则将其发送到 Gateway web handler处理。 handler处理请求时会经过一系列的过滤器链。 过滤器链被虚线划分的原因是过滤器链可以在发送代理请求之前或之后执行过滤逻辑。 先执行所有 “pre”过滤器逻辑,然后进行代理请求。 在发出代理请求之后,收到代理服务的响应之后执行 “post”过滤器逻辑。这跟 Zuul的处理过程很类似。在执行所有 “pre”过滤器逻辑时,往往进行了鉴权、限流、日志输出等功能,以及请求头的更改、协议的转换;转发之后收到响应之后,会执行所有“post”过滤器的逻辑,在这里可以响应数据进行了修改,比如响应头、协议的转换等。在上面的处理过程中,有一个重要的点就是将请求和路由进行匹配,这时候就需要用到predicate,它是决定了一个请求走哪一个路由。
四、Spring Cloud Gateway 的几个概念
【1】Route 路由:Gateway的基本构建模块,它由ID、目标URL、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。Gateway 依赖如下:
**Route 路由-动态路由实现:**网关管理平台实现增删改查路由信息到 mysql。网关管理平台发布,发布后将路由信息通过配置中心 openapi 保存到配置中心服务中。网关监听配置中心配置变化,动态刷新到内存中。
【2】**Predicate 断言:**这是一个Java 8 Function Predicate。输入类型是 Spring Framework ServerWebExchange。允许开发人员匹配来自 HTTP请求的任何内容,例如 Header或参数。Predicate 接受一个输入参数,返回一个布尔值结果。Spring Cloud Gateway内置了许多Predict,这些 Predict的源码在 org.springframework.cloud.gateway.handler.predicate包中,如果读者有兴趣可以阅读一下。现在列举各种 Predicate如下图:
在上图中,有很多类型的 Predicate,比如说时间类型的 Predicated(AfterRoutePredicateFactory BeforeRoutePredicateFactory BetweenRoutePredicateFactory),当只有满足特定时间要求的请求会进入到此 Predicate中,并交由 Router处理;Cookie类型的 CookieRoutePredicateFactory,指定的 Cookie满足正则匹配,才会进入此 Router。以及host、method、path、querparam、remoteaddr类型的 Predicate,每一种 Predicate都会对当前的客户端请求进行判断,是否满足当前的要求,如果满足则交给当前请求处理。如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。
Predicate 断言配置:
1 server:
2 port: 8080
3 spring:
4 application:
5 name: api-gateway
6 cloud:
7 gateway:
8 routes:
9 - id: gateway-service
10 uri: https://www.baidu.com
11 order: 0
12 predicates:
13 - After=2017-01-20T17:42:47.789-07:00[America/Denver]
14 - Host=**.foo.org
15 - Path=/headers
16 - Method=GET
17 - Header=X-Request-Id, \d+
18 - Query=foo, ba.
19 - Query=baz
20 - Cookie=chocolate, ch.p
在上面的配置文件中,配置了服务的端口为8080,配置 spring cloud gateway 相关的配置,id标签****配置的是 router的 id,每个 router都需要一个唯一的id,uri配置****的是将请求路由到哪里,本案例全部路由到 https://www.baidu.com。
Predicates:After=2017-01-20T17:42:47.789-07:00[America/Denver] 会被解析成 PredicateDefinition对象 (name =After ,args= 2017-01-20T17:42:47.789-07:00[America/Denver])。需要注意的是 Predicates的 After这个配置,遵循契约大于配置的思想,它实际被 AfterRoutePredicateFactory这个类所处理,这个 After就是指定了它的 Gateway web handler类为 AfterRoutePredicateFactory,同理,其他类型的 Predicate也遵循这个规则。当请求的时间在这个配置的时间之后,请求会被路由到指定的URL。跟时间相关的 Predicates还有 Before Route Predicate Factory、Between Route Predicate Factory,读者可以自行查阅官方文档,再次不再演示。
**Query=baz:**Query 的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由。经过测试发现只要请求汇总带有 baz参数即会匹配路由[localhost:8080?baz=x&id=2],不带 baz参数则不会匹配。
**Query=foo, ba.:**这样只要当请求中包含 foo属性并且参数值是以 ba开头的长度为三位的字符串才会进行匹配和路由。使用 curl 测试,命令行输入:curl localhost:8080?foo=bab 测试可以返回页面代码,将 foo的属性值改为 babx 再次访问就会报 404,证明路由需要匹配正则表达式才会进行路由。
**Header=X-Request-Id, \d+:**使用 curl 测试,命令行输入:curl http://localhost:8080 -H “X-Request-Id:88” 则返回页面代码证明匹配成功。将参数-H "X-Request-Id:88"改为-H "X-Request-Id:spring"再次执行时返回404证明没有匹配。
【3】**Filter 过滤器:方案一:**写死在代码中
1 @Bean
2 public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
3 return builder.routes()
4 //openapi路由转发
5 .route("openapi_route", p -> p.path( "/openapi/**").filters(f->f.removeRequestHeader("Expect"))
6 .uri("lb://order-openapi-service"))
7 .build();
8 }
**方案二:**配置文件(yml)
1 # gateway 的配置形式
2 routes:
3 - id: order-service #路由ID,没有规定规则但要求唯一,建议配合服务名。
4 uri: lb://order-service
5 predicates:
6 - Path=/order/**
7 filters:
8 - ValidateCodeGatewayFilter
Filter 过滤器:Filter 按处理顺序 Pre Filter / Post Filter
**Filter 按作用范围分为:**GlobalFilter 全局过滤器。GatewayFilter 指定路由的过滤器。
**Filter 过滤器-扩展自定义Filter:**Filter 支持通过 spi 扩展。实现 GatewayFilter,Ordered接口。Filter 方法:过滤器处理逻辑。getOrder:定义优先级,值越大优先级越低。
1 public class TokenFilter implements GlobalFilter, Ordered {
2
3 Logger logger=LoggerFactory.getLogger( TokenFilter.class );
4 @Override
5 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
6 String token = exchange.getRequest().getQueryParams().getFirst("token");
7 if (token == null || token.isEmpty()) {
8 logger.info( "token is empty..." );
9 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
10 return exchange.getResponse().setComplete();
11 }
12 return chain.filter(exchange);
13 }
14
15 @Override
16 public int getOrder() {
17 return -100;
18 }
19 }
五、快速开始
网关启动步骤(代码演示):
【1】添加依赖
1 <dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-gateway</artifactId>
4 </dependency>
【2】配置文件
1 spring:
2 cloud:
3 gateway:
4 routes:
5 - id: product #路由ID,根据业务自行定义
6 uri: http://bin.org:80/get #路由的地址
7 predicates:
8 - Path=/**
9 #开启根据服务名进行转发,需要将服务注册到注册中心
10 discovery:
11 locator:
12 enabled: true
六、Gateway 限流
在 Spring Cloud Gateway中,有 Filter过滤器,因此可以在“pre”类型的 Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了 RequestRateLimiterGatewayFilterFactory这个类,适用 Redis和 Lua脚本实现了令牌桶【链接】的方式。具体实现逻辑在 RequestRateLimiterGatewayFilterFactory类中,Lua脚本在如下图所示的文件夹中:
具体源码不打算在这里讲述,读者可以自行查看,代码量较少,先以案例的形式来讲解如何在 Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。首先在工程的 pom文件中引入 Gateway的起步依赖和 Redis的 Reactive依赖,代码如下:
1 <dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-gateway</artifactId>
4 </dependency>
5
6 <dependency>
7 <groupId>org.springframework.boot</groupId>
8 <artifatId>spring-boot-starter-data-redis-reactive</artifactId>
9 </dependency>
在配置文件中做以下的配置:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: lb://PRODUCTCENTOR # PRODUCTCENTOR是注册到注册中心的服务名,格式为 lb://服务名
predicates:
- Path=/**
filters:
- StripPrefix=1
- name: RequestRateLimiter #拦截器,会对上述的请求进行拦击
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: gateway-limiter
redis:
host: localhost
port: 6379
database: 0
过滤器 StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。 StripPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view。
在上面的配置文件,指定程序的端口为8081,配置了 Redis的信息,并配置了 RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
【1】burstCapacity:令牌桶总容量。
【2】replenishRate:令牌桶每秒填充平均速率。
【3】key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
KeyResolver需要实现 resolve方法,比如根据 Hostname进行限流,则需要用 hostAddress去判断。实现完 KeyResolver之后,需要将这个类的 Bean注册到 Ioc容器中。
1 public class HostAddrKeyResolver implements KeyResolver {
2
3 @Override
4 public Mono<String> resolve(ServerWebExchange exchange) {
5 return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
6 }
7
8 }
9
10 @Bean
11 public HostAddrKeyResolver hostAddrKeyResolver() {
12 return new HostAddrKeyResolver();
13 }
可以根据 URL去限流,这时 KeyResolver代码如下:
1 public class UriKeyResolver implements KeyResolver {
2
3 @Override
4 public Mono<String> resolve(ServerWebExchange exchange) {
5 return Mono.just(exchange.getRequest().getURI().getPath());
6 }
7
8 }
9
10 @Bean
11 public UriKeyResolver uriKeyResolver() {
12 return new UriKeyResolver();
13 }
也可以以用户的维度去限流:
1 @Bean
2 KeyResolver userKeyResolver() {
3 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
4 }
用 jmeter进行压测,配置 10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过 redis客户端去查看 redis中存在的key。如下:
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(“user”));
4 }
用 jmeter进行压测,配置 10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过 redis客户端去查看 redis中存在的key。如下:
![img](https://img-blog.csdnimg.cn/20201014232351595.png)[外链图片转存中...(img-CSSPYwqO-1616254583168)]
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看 lua源代码。