Gateway服务网关
官网地址:https://spring.io/projects/spring-cloud-gateway
1、什么是Spring Cloud Gateway
来自官网的介绍:
该项目提供了一个库,用于在 Spring WebFlux 之上构建 API 网关。Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。
特征
Spring Cloud Gateway 特点:
- 基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 构建
- 能够匹配任何请求属性的路由。
- 谓词和过滤器特定于路由。
- 断路器集成。
- Spring Cloud Discovery客户端集成
- 易于编写谓词和过滤器
- 请求速率限制
- 路径重写
2、什么是服务网关
API Gateway(API网关),顾名思义,是出现在系统边界上的一个面向API的、串行集中式的强管控制服务,这里的边界是企业IT系统的边界,可以理解为企业级应用防火墙,主要起到隔离外部访问与内部系统的作用。在微服务概念流行前,API网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供定制的API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。对于服务数量众多,复杂度比较高、规模比较大的业务来说,引入API网关也有一些列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
- 聚合后台服务,节省流量,提供新能,提升用户体验
- 提供安全、流控、过滤、缓存、计费、监控等API管理功能
3、Gateway实现API网关
3.1、核心概念
路由(route):路由是网关最基础的部分,路由信息由ID、目标URI、一组断言和一组过滤器组成。如果断言路由为真,则转发请求。
断言(predicate):Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于Http Request中的任何信息,比如请求头头和参数等。
过滤器(filter):一个标准的Spring Web Filter。Spring Cloud Gateway中的filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器将会对请求和响应进行处理。
3.2、工作原理
客户端向Spring Cloud Gateway发出请求。再由网关处理程序Gateway Handle Mapping确定与请求相匹配的路由,将其发送到网关Web处理程序Gateway Web Handle,该程序通过指定的过滤器链将请求发送到实际的服务执行业务逻辑,然后返回。过滤器由虚线分隔的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有pre过滤器逻辑均被执行。然后发出代理请求。发出代理请求后,将运行post过滤器逻辑。
3.3、搭建网关服务
示例代码地址:https://gitee.com/junweihu/spring-cloud-alibaba-demo
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
配置
server:
port: 9000
spring:
application:
name: gateway-server
启动类
@SpringBootApplication
public class GatewayServerApplication
{
public static void main( String[] args )
{
SpringApplication.run(GatewayServerApplication.class, args);
}
}
3.4、配置路由规则
server:
port: 9000
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: http://localhost:9001 # 目标uri,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
#- Query=token # 请求参数中包含token
#- Query=token, abc. # 请求参数中包含token,且参数值满足正则表达式
#- Method=GET # 匹配任意get请求
#- After=2022-05-23T15:20:00.000+08:00[Asia/Shanghai] # 匹配中国上海时间
#- RemoteAddr=192.168.127.1/24 # 匹配远程请求地址,24表示子网掩码
#- Header=X-Request-Id, \d+ # 请求头中包含X-Request-Id,且参数值满足正则表达式
请求http://localhost:9000/product/get/444将会路由至http://localhost:9001/product/get/444
4、路由规则
Spring Cloud Gatway创建Route对象时,使用Route Predicate Factory创建Predicate对象,Predicate对象可以赋值给Route。
- Spring Cloud Gatway包含许多内置的Route Predicate Factory,所有这些断言都匹配HTTP请求的不同属性
- 多个Route Predicate Factory可以通过逻辑与(and)结合起来一起使用
Route Predicate Factory包含的主要实现类如图所示,包括Datetime、请求的远端地址、路由权重、请求头、host地址、请求方法、请求路径和请求参数等类型的路由断言。
使用方法及说明:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
5、动态路由
动态路由其实就是面向服务的路由,根据serviceId自动从注册中心获取服务列表并转发,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或者移除服务实例时不用修改Gateway的路由配置。
5.1、动态获取uri
添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
修改配置文件
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名,以负载均衡方式调用
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
5.2、服务名称转发
修改配置
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true # 是否与服务发现组件结合,通过serviceId转发到具体服务实例
lower-case-service-id: true # 是否将服务名称转小写
此时通过 网关地址:端口/服务名称/接口资源 可以转发到具体的服务,例如:http://localhost:9000/nacos-consumer/order/888
6、过滤器
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
- GatewayFilter:网关过滤器,需要通过spring.cloud.routes.filters配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上。
- GlobalFilter:全局过滤,不需要在配置文件中配置,作用在所有路由上,最终通过GatewayFilterAdater包装成GatewayFilterChain可识别的过滤器,它是请求业务及路由的uri转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
6.1、网关过滤器GatewayFilter
网关过滤器用于拦截并链式处理Web请求,可以实现横切与应用无关的需求,比如:安全、访问超时的设置等。修改传入的HTTP请求或传出的HTTP响应。Spring Cloud Gataway包含许多内置的网关过滤工厂,包括头部过滤器、路径过滤器、Hystrix过滤器和重写请求URL的过滤器,还有参数和状态码等其它类型的过滤器。根据过滤工厂的用途来划分,可以分为以下几种,Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter、Hystrix和token。
使用方法及说明:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
6.1.1、Path 过滤器
RewritePathGatewayFilterFactory
Path路径过滤器可以实现URL重写,通过重写URL可以实现隐藏实际路径提高安全性。
RewritePath 网关过滤器工厂采用路径正则表达式参数和替换参数,使用Java正则表达式来灵活地重写请求路径。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/api-gateway/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- RewritePath=/api-gateway/?(?<segment>.*), /$\{segment} # 将/api-gateway/product替换成/product
PrefixPathGatewayFilterFactory
PrefixPath 网关过滤器工厂为匹配的URI添加指定前缀。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- PrefixPath=/product # 将/port重写为/product/port
StripPrefixGatewayFilterFactory
StripPrefix网关过滤工厂采用一个参数,该参数表示在将请求发送到下游之前从请求中剥离的路径个数。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/api-gateway/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- StripPrefix=2 # 将api-gateway/test/product/port截取成/product/port
SetPathGatewayFilterFactory
SetPath网关过滤工厂采用路径模版参数。它提供了一种通过模板化路径来操作请求路径的简单方法,使用了Spring框架的uri模版,允许多个匹配段。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/api-gateway/product/{segment} # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- SetPath=/product/{segment} # 将/api-gateway/product/port设置成/product/port
6.1.2、Parameter 过滤器
AddRequestParameterGatewayFilterFactory
AddRequestParameter网关过滤工厂会将指定参数添加至匹配到的下游请求中。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- AddRequestParameter=token, blue # 在下游请求中添加参数token=blue
6.1.3、Status 过滤器
SetStatus网关过滤工厂采用单个状态参数,该参数必须是有效的Spring HttpStatus。它可以是整数404或者字符串枚举NOT_FOUND。
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由规则
- id: product-server # 路由ID,唯一
uri: lb://nacos-provider # 目标uri,路由到微服务的地址。lb://服务名
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应url的请求,将匹配到的请求追加在目标uri之后
filters:
- SetStatus=401
6.2、全局过滤器GlobalFilter
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdater包装成GatewayFilterChain可是别的过滤器,它是请求业务以及路由的uri转换为真实业务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
6.3、自定义过滤器
6.3.1、自定义网关过滤器
自定义网关过滤器需要实现以下两个接口:GatewayFilter,Ordered。
创建网关过滤器
public class CustomGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI uri = exchange.getRequest().getURI();
System.out.println("请求路径:"+uri.toString());
// 继续向下执行
return chain.filter(exchange);
}
/**
* 数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
注册过滤器
@Configuration
public class GatewayRoutesConfiguration {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r->r.path("/product/**") // 断言
.filters(f->f.filter(new CustomGatewayFilter())) // 注册自定义网关过滤器
.uri("lb://nacos-provider")) // 目标uri
.build();
}
}
6.3.2、自定义全局过滤器
自定义全局过滤器需要实现以下两个接口:GlobalFilter,Ordered。通过全局过滤器可以实现权限校验,安全性验证等功能。
创建全局过滤器
@Slf4j
@Component
public class AccessFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isEmpty(token)) {
log.warn("token is null");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String message = "{\"message\":\""+HttpStatus.UNAUTHORIZED.getReasonPhrase()+"\"}";
DataBuffer wrap = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(wrap));
}
log.info("token is ok");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
无需注册全局过滤器,添加@Component注解即可。
路由过滤器的执行顺序
SringCloudGateWay 中,有三种过滤器:
- 默认过滤器 default-filters
- 针对某个路由生效的局部过滤器 filters
- 使用Java代码编写的全局过滤器
过滤器的执行顺序
由上图所示,过滤器的执行顺序为:默认过滤器 ——> 当前路由过滤器 ——> 全局过滤器
7、网关限流
7.1、限流算法
常见的限流算法有:
- 计数器算法
- 漏斗(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
- 滑动窗口算法
7.1.1、计数器算法
计数器算法是限流算法里最简单也是醉容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100且该请求与第一个请求的时间间隔还在1分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于1分钟,重置counter重新计数,具体算法的示意图如下:
这个算法虽然简单,但是有一个十分致命的临界问题,看下图:
从上图中我们可以看到,假设有一个恶意用户,在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒内,瞬间发送了200个请求。我们刚才规定的是1分钟最多发送100个请求,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
其次该算法还存在资源浪费的情况,我们的预想是希望100个请求可以均匀的分散在这1分钟内,假设1秒内就到达请求上限了,那么在剩余的59秒内我们的服务就会处于闲置状态。
7.1.2、滑动窗口算法
滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。
一张图解释滑动窗口算法,如下:
假设单位时间是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单位时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。然后呢,每个小周期,都有自己独立的计数器,如果请求是0.83s到达的,0.8~1.0s对应的计数器就会加1。
我们来看下滑动窗口是如何解决临界问题的?
假设我们1s内的限流阀值还是5个请求,0.81.0s内(比如0.9s的时候)来了5个请求,落在黄色格子里。时间过了1.0s这个点之后,又来5个请求,落在紫色格子里。如果**是固定窗口算法,是不会被限流的**,但是**滑动窗口的话,每过一个小周期,它会右移一个小格**。过了1.0s这个点后,会右移一小格,当前的单位时间段是0.21.2s,这个区域的请求已经超过限定的5了,已触发限流啦,实际上,紫色格子的请求都被拒绝啦。
当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
7.1.3、漏桶算法
漏桶算法可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率出水,如果漏桶溢出则丢弃数据。
漏桶算法是使用队列机制实现的。
-
队列接收到准备转发的数据包。
-
队列被调度,得到转发机会。由于队列配置了流量整形,队列中的数据包首先进入漏桶中。
-
根据数据包到达漏桶的速率与漏桶的输出速率关系,确定数据包是否被转发。
如果到达速率≤输出速率,则漏桶不起作用。
如果到达速率>输出速率,则需考虑漏桶是否能承担这个瞬间的流量。
- 若数据包到达的速率-漏桶流出的速率≤配置的漏桶突发速率,则数据包可被不延时的送出。
- 若数据包到达的速率-漏桶流出的速率>配置的漏桶突发速率,则多余的数据包被存储到漏桶中。暂存在漏桶中的数据包在不超过漏桶容量的情况下延时发出。
- 若数据包到达的速率-漏桶流出的速率>配置的漏桶突发速率,且数据包的数量已经超过漏桶的容量,则这些数据包将被丢弃。
7.1.4、令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则等待可用的令牌,或者直接拒绝。放令牌这个动作是不断进行的,如果桶中的令牌数量达到上限,就丢弃令牌。
场景:桶中一直有大量可用的令牌,这时进来的请求可以直接拿到令牌执行,比如设置QPS为100/s,那么限流器初始化完成一秒后,桶中就已经有了100个令牌,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
漏桶算法与令牌桶算法的区别在于:
- 漏桶算法能够强行限制数据的传输速率。
- 令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。
需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。
7.2、Gateway 限流
Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory过滤工厂,使用redis和lua脚本实现令牌桶限流。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
添加配置
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: product-service
uri: lb://nacos-provider
predicates:
- Path=/product/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@pathKeyResolver}" # 使用SpEl表达式按名称引用bean
redis:
timeout: 3000 # 连接超时时间
host: localhost
port: 6379
password: root123/
database: 0
lettuce:
pool:
max-active: 1024 # 最大连接数
max-wait: 5000 # 最大连接阻塞等待时间
max-idle: 200 # 最大空闲连接数
min-idle: 5 # 最小空闲连接数
7.2.1、限流规则
7.2.1.1、uri 限流
key-resolver: "#{@parameterKeyResolver}"
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
7.2.1.2、参数限流
key-resolver: "#{@parameterKeyResolver}"
@Bean
public KeyResolver parameterKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("token"));
}
7.2.1.3、IP 限流
key-resolver: "#{@ipKeyResolver}"
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
7.3、Sentinel 限流
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
添加依赖
<!-- sentinel的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- spring cloud gateway整合sentinel的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!--spring cloud gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置文件
application.yml
server:
port: 9011
spring:
application:
name: gateway-server-sentinel
cloud:
sentinel: # 整合sentinel,配置sentinel控制台的地址
transport:
dashboard: localhost:8099 # 配置控制台地址
gateway:
discovery:
locator:
enabled: true # 是否与服务发现组件进行结合,通过serviceId转发到具体服务实例
lower-case-service-id: true
routes:
- id: order-service
uri: lb://nacos-provider
predicates:
- Path=/product/**
- id: test-service
uri: lb://nacos-provider
predicates:
- Path=/test/**
bootstrap.yml
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
7.3.1、route维度限流
即在配置文件中配置的路由条目,资源名为对应的routeId
,这种属于粗粒度的限流,一般是对某个微服务进行限流。
快速点击访问http://localhost:9011/product/get/78,限流结果如下:
7.3.2、自定义API维度限流
用户可以利用Sentinel提供的API来自定义一些API分组,这种属于细粒度的限流,针对某一类的uri进行匹配限流,可以跨多个微服务。
7.3.3、自定义异常信息
从上面示例可以看到默认返回的异常信息是Blocked by Sentinel: ParamFlowException。介绍两种方式定制异常信息。
7.3.3.1、配置文件定制异常信息
spring:
application:
name: gateway-server-sentinel
cloud:
sentinel:
scg: # 配置限流之后,响应内容
fallback:
mode: response # 两种模式,一种是response返回文字提示信息,一种是redirect,重定向跳转,需要同时配置redirect(跳转的uri)
# redirect: http://www.baidu.com # 跳转的URL
response-status: 200
response-body: '{"code": 200,"message": "请求失败,稍后重试!"}'
7.3.3.2、编码定制异常信息
@Configuration
public class GatewayConfig {
/**
* 自定义限流处理器
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockHandler = (serverWebExchange, throwable) -> {
Map map = new HashMap();
map.put("code",200);
map.put("message","请求失败,稍后重试!");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(map));
};
GatewayCallbackManager.setBlockHandler(blockHandler);
}
}