优质博文:IT-BLOG-CN
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
”集合,可以实现一系列与核心业务无关的横切面功能,如安全认证、限流熔断、日志监控。
网关在系统中所处的位置:
二、快速开始
网关启动步骤(代码演示):
【1】添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
【2】配置文件
spring:
cloud:
gateway:
discovery: # 启用通过服务发现自动创建路由。
locator:
enabled: true
routes:
- id: example_route #路由ID,根据业务自行定义
uri: lb://example-service #目标服务的地址,这里使用 lb:// 前缀来表示负载均衡。可以是 HTTP(s) URI 或其他协议的 URI - http://bin.org:80/get。
predicates:
- Path=/example/** #predicates: 谓词数组,用于匹配请求。常见的谓词包括 Path、Method、Header 等。
filters: #过滤器数组,用于在请求被转发到目标服务之前和之后进行处理。常见的过滤器包括 AddRequestHeader、StripPrefix、RewritePath 等。
- AddRequestHeader=X-Example, ExampleValue #过滤器会在请求头中添加 X-Example,值为 ExampleValue。
- StripPrefix=1 #过滤器会移除路径中的第一个前缀。例如,请求路径 /example/test 会变成 /test。
- id: rate_limited_route
uri: http://ratelimited.org
predicates:
- Path=/ratelimited/**
filters:
- RequestRateLimiter=redis-rate-limiter # 限流:通过 RequestRateLimiter 过滤器实现
- id: retry_route
uri: http://retry.org
predicates:
- Path=/retry/**
filters:
- Retry=5 #重试:通过 Retry 过滤器实现。
default-filters: #default-filters 是全局过滤器数组,适用于所有路由。这个过滤器会在所有响应中添加 X-Response-Default 头,值为 Default。
- AddResponseHeader=X-Response-Default, Default
globalcors: #globalcors 用于配置全局的 CORS(跨域资源共享)设置。
corsConfigurations: #corsConfigurations: 定义 CORS 配置的路径模式。
'[/**]': #匹配所有路径。
allowedOrigins: "*" #允许的源,* 表示允许所有源。
allowedMethods: #允许的 HTTP 方法,包括 GET、POST、DELETE 和 PUT。
- GET
- POST
- DELETE
- PUT
三、Spring Cloud GateWay 架构图
客户端向Spring Cloud Gateway
发出请求。 在Gateway Handler Mapping
中找到请求相对匹配路由(这个时候就用到predicate
),则将其发送到Gateway web handler
处理。 handler
处理请求时会经过一系列的过滤器链。 过滤器链被虚线划分的原因是过滤器链可以在发送代理请求之前或之后执行过滤逻辑。 先执行所有pre
过滤器逻辑,然后进行代理请求。 在发出代理请求之后,收到代理服务的响应之后执行post
过滤器逻辑。这跟Zuul
的处理过程很类似。在执行所有pre
过滤器逻辑时,往往进行了鉴权、限流、日志输出等功能,以及请求头的更改、协议的转换;转发之后收到响应之后,会执行所有post
过滤器的逻辑,在这里可以响应数据进行了修改,比如响应头、协议的转换等。在上面的处理过程中,有一个重要的点就是将请求和路由进行匹配,这时候就需要用到predicate
,它是决定了一个请求走哪一个路由。
四、SpringColoud GateWay 核心组件
集合上面的配置和架构图进行说明
【1】Route
路由: Gateway
的基本构建模块,它由ID
、目标URL
、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。
Route
路由-动态路由实现原理: 配置变化Apollo
+ 服务地址实例变化Nacos
。Spring Cloud Gateway
通过RouteDefinitionLocator
和RouteRefreshListener
等组件实现动态路由。
先看下配置信息,方便后面原理的理解:SpringCloudGateway bootstrap.yml
的配置如下:
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
apollo:
bootstrap:
enabled: true
meta: ${APOLLO_META:localhost:8080}
application.yml
的配置如下:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
apollo:
bootstrap:
namespaces: application # 1、登录 Apollo 控制台。 2、创建一个新的配置,例如 application.yml。 3、内容就是上看配置的SpringCloud Gateway 配置的路由信息
1、RouteDefinitionLocator
:Spring Cloud Gateway
启动时,会通过RouteDefinitionLocator
从Apollo
加载初始的路由定义。
2、DiscoveryClientRouteDefinitionLocator
:使用Nacos
进行服务发现,从Nacos
获取动态路由定义。
3、RouteDefinitionRepository
:加载的路由定义会存储在RouteDefinitionRepository
中,供后续路由匹配使用。
4、RouteRefreshListener
:监听路由定义的变化事件(如配置更新、服务实例变化等)。当监听到路由定义变化事件时,触发路由刷新操作,更新网关的路由规则,重新加载并应用新的路由配置。
GatewayHandlerMapping
根据预先配置的路由信息和请求的属性(如路径、方法、头部信息等)来确定哪个路由与请求匹配。它使用谓词Predicates
来进行匹配判断。
【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 断言配置:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- 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
过滤器:方案一:写死在代码中
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
//openapi路由转发
.route("openapi_route", p -> p.path( "/openapi/**").filters(f->f.removeRequestHeader("Expect"))
.uri("lb://order-openapi-service"))
.build();
}
方案二:配置文件yml
# gateway 的配置形式
routes:
- id: order-service #路由ID,没有规定规则但要求唯一,建议配合服务名。
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- ValidateCodeGatewayFilter
Filter
过滤器:Filter
按处理顺序Pre Filter / Post Filter
Filter
按作用范围分为: GlobalFilter
全局过滤器。GatewayFilter
指定路由的过滤器。
Filter
过滤器-扩展自定义Filter
: Filter
支持通过spi
扩展。实现GatewayFilter
,Ordered
接口。
Filter
方法: 过滤器处理逻辑。getOrder
:定义优先级,值越大优先级越低。
全局过滤器示例: 创建一个全局过滤器类,实现GlobalFilter
接口:
public class TokenFilter implements GlobalFilter, Ordered {
Logger logger=LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token is empty..." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
// // 过滤器的执行顺序,值越小优先级越高
return -100;
}
}
自定义路由过滤器示例: 创建自定义的路由过滤器,可以实现GatewayFilter
接口:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyCustomFilter extends AbstractGatewayFilterFactory<MyCustomFilter.Config> {
public MyCustomFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 前置过滤逻辑
System.out.println("Custom Pre Filter executed");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 后置过滤逻辑
System.out.println("Custom Post Filter executed");
}));
};
}
public static class Config {
// 配置属性
}
}
在配置文件中使用自定义过滤器:
spring:
cloud:
gateway:
routes:
- id: my_route
uri: http://httpbin.org:80
predicates:
- Path=/get
filters:
- name: MyCustomFilter
五、Gateway 限流
在Spring Cloud Gateway
中,有Filter
过滤器,因此可以在“pre”
类型的Filter
中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway
官方就提供了RequestRateLimiterGatewayFilterFactory
这个类,适用Redis
和Lua
脚本实现了令牌桶链接的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory
类中,Lua
脚本在如下图所示的文件夹中:
以案例的形式来讲解如何在SpringCloud Gateway
中使用内置的限流过滤器工厂来实现限流。首先在工程的pom
文件中引入Gateway
的起步依赖和Redis
的Reactive
依赖,代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</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
容器中。
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
可以根据URL
去限流,这时KeyResolver
代码如下:
public class UriKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}
@Bean
public UriKeyResolver uriKeyResolver() {
return new UriKeyResolver();
}
也可以以用户的维度去限流:
// 省略部分代码
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
用jmeter
进行压测,配置10 thread
去循环请求lcoalhost:8081
,循环间隔1s
。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis
客户端去查看redis
中存在的key
。如下:
127.0.0.1:6379> keys *
1> "request_rate_limiter.<127.0.0.1>.timestamp"
2> "request_rate_limiter.<127.0.0.1>.tokens"
可见,RequestRateLimiter
是使用Redis
来进行限流的,并在redis
中存储了2
个key
。关注这两个key
含义可以看lua
源代码。
六、Zuul 与 Spring Cloud Gateway 比较
优点 | 缺点 |
---|---|
Gateway | 1、线程开销小2、使用轻量级 Netty 异步IO实现通信3、支持各种长连接,WebSocket4、Spring 官方推荐,重点支持,功能较 Zuul更丰富,支持限流监控等 |
Zuul | 1、编码模型简单2、开发调试运维简单 |
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
的表现要好
七、SpringCloud GateWay 与 Nginx 组合使用
因为和
GateWay
相关所以这里介绍一下
Nginx
和Spring Cloud Gateway
可以组合使用,以实现高效的负载均衡和网关功能。Nginx
通常用于处理静态内容、SSL
终止、负载均衡等,而Spring Cloud Gateway
主要用于动态路由、过滤和服务网关功能。下面是一个基本的配置示例,展示了如何将Nginx
和Spring Cloud Gateway
结合使用。这里主要说下Nginx
中的配置:
配置Nginx
作为反向代理,将外部请求转发到Spring Cloud Gateway
。
nginx.conf
配置示例
http {
upstream gateway { # 定义一个名为 gateway 的上游服务器组,包含 Spring Cloud Gateway 的地址(localhost:8080)。
server localhost:8080;
}
server { # 配置 Nginx 服务器块。
listen 80; # 监听80端口。
location / { # 匹配所有请求。
proxy_pass http://gateway; # http://gateway: 将请求转发到上游服务器组 gateway。
proxy_set_header Host $host; # 设置一些头信息,用于保持客户端信息。
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
工作流程:
【1】客户端请求:客户端发送请求到Nginx
。
【2】Nginx
转发:Nginx
接收到请求后,将其转发到Spring Cloud Gateway
。
【3】Spring Cloud Gateway
路由:Spring Cloud Gateway
根据配置文件中的路由规则,将请求转发到对应的微服务。
【4】微服务响应:微服务处理请求并返回响应,通过Spring Cloud Gateway
和Nginx
返回给客户端。
SpringCloud GateWay 与 Nacos 组合使用
bootstrap.yml
用于配置Nacos
的基本信息:
spring:
application:
name: gateway-service #设置网关服务的名称。
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848} #Nacos 服务发现的地址。 ${NACOS_SERVER_ADDR}: 使用环境变量配置 Nacos 服务器地址,方便在不同环境中切换。
config:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848} #Nacos 配置管理的地址。
file-extension: yaml
可以将Spring Cloud Gateway
的配置放在Nacos
配置中心,这样可以实现配置的集中管理和动态更新。在Nacos
中创建配置:
【1】登录Nacos
控制台。
【2】创建一个新的配置,例如gateway-service.yaml
。
【3】在配置文件中添加路由规则,例如:
spring:
cloud:
gateway:
routes:
- id: example-service
uri: lb://example-service
predicates:
- Path=/example/**
- id: another-service
uri: lb://another-service
predicates:
- Path=/another/**