官网地址:https://spring.io/projects/spring-cloud-gateway
1、什么是Spring Cloud Gateway来自官网的介绍:
2、什么是服务网关该项目提供了一个库,用于在 Spring WebFlux 之上构建 API 网关。Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。
特征Spring Cloud Gateway 特点:
- 基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 构建
- 能够匹配任何请求属性的路由。
- 谓词和过滤器特定于路由。
- 断路器集成。
- Spring Cloud Discovery客户端集成
- 易于编写谓词和过滤器
- 请求速率限制
- 路径重写
API Gateway(API网关),顾名思义,是出现在系统边界上的一个面向API的、串行集中式的强管控制服务,这里的边界是企业IT系统的边界,可以理解为企业级应用防火墙,主要起到隔离外部访问与内部系统的作用。在微服务概念流行前,API网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供定制的API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。对于服务数量众多,复杂度比较高、规模比较大的业务来说,引入API网关也有一些列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
- 聚合后台服务,节省流量,提供新能,提升用户体验
- 提供安全、流控、过滤、缓存、计费、监控等API管理功能
路由(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
依赖
org.springframework.cloud spring-cloud-starter-gateway
配置
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
@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 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注解即可。
7、网关限流 7.1、限流算法常见的限流算法有:
- 计数器算法
- 漏斗(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
- 滑动窗口算法
计数器算法是限流算法里最简单也是醉容易实现的一种算法。比如我们规定,对于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、漏桶算法漏桶算法可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率出水,如果漏桶溢出则丢弃数据。
漏桶算法是使用队列机制实现的。
-
队列接收到准备转发的数据包。
-
队列被调度,得到转发机会。由于队列配置了流量整形,队列中的数据包首先进入漏桶中。
-
根据数据包到达漏桶的速率与漏桶的输出速率关系,确定数据包是否被转发。
如果到达速率≤输出速率,则漏桶不起作用。
如果到达速率>输出速率,则需考虑漏桶是否能承担这个瞬间的流量。
- 若数据包到达的速率-漏桶流出的速率≤配置的漏桶突发速率,则数据包可被不延时的送出。
- 若数据包到达的速率-漏桶流出的速率>配置的漏桶突发速率,则多余的数据包被存储到漏桶中。暂存在漏桶中的数据包在不超过漏桶容量的情况下延时发出。
- 若数据包到达的速率-漏桶流出的速率>配置的漏桶突发速率,且数据包的数量已经超过漏桶的容量,则这些数据包将被丢弃。
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则等待可用的令牌,或者直接拒绝。放令牌这个动作是不断进行的,如果桶中的令牌数量达到上限,就丢弃令牌。
场景:桶中一直有大量可用的令牌,这时进来的请求可以直接拿到令牌执行,比如设置QPS为100/s,那么限流器初始化完成一秒后,桶中就已经有了100个令牌,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
漏桶算法与令牌桶算法的区别在于:
- 漏桶算法能够强行限制数据的传输速率。
- 令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。
需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。
7.2、Gateway 限流Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory过滤工厂,使用redis和lua脚本实现令牌桶限流。
添加依赖
org.springframework.boot spring-boot-starter-data-redis-reactive org.apache.commons commons-pool2
添加配置
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());
}



