从浅入深 学习 SpringCloud 微服务架构(十二)网关限流算法和 SCG 网关 filter 限流。
一、网关限流算法:
1、网关限流算法:常见的限流算法
- 网关限流算法:计数器算法
- 网关限流算法:漏桶算法
- 网关限流算法:令牌桶算法
2、网关限流算法:计数器算法
计数器限流算法:是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加 1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。
3、网关限流算法:漏桶算法
1)漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
2)为了更好的控制流量,漏桶算法需要通过两个变量进行控制:
- 一个是桶的大小,支持流量突发增多时可以存多少的水(burst),
- 另一个是水桶漏洞的大小(rate)。
3)漏桶算法保护的不是自己网关,而是其他的微服务。
4、网关限流算法:令牌桶算法
1)令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。
放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,
比如设置 qps 为 100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
2)令牌桶算法主要保护的是自己网关,而不是其他的微服务。
二 SCG 网关:filter 限流
1、基于 Filter 的网关限流
1)SpringCloudGateway 官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂 RequestRateLimiterGatewayFi1terFactory 实现。
在过滤器工厂中是通过 Redis 和 lua 脚本结合的方式进行流量控制。
2)基于 Filter 的网关限流 步骤:
- 准备工作:需要 redis 数据库。
- 在工程中引入 redis 相应的依赖坐标,如 在子工程 api_gateway_service(子模块)的 pom.xml 配置文件中引入 redis 的依赖坐标。
- 修改网关中的 application.yml 配置文件。
- 配置 redis 中 key 的解析器 KeySesolver
2、下载安装 redis 数据库,并启动服务端程序,和客户端程序。
1)redis-4.0.2.3 windows 下载:
https://github.com/tporadowski/redis/releases
2)下载完成后,在 redis 安装目录,双击启动服务端程序 redis-server.exe 和 客户端程序 redis-cli.exe
3)在 客户端程序 redis-cli.exe 输入: monitor 时刻监测 服务端。
3、在子工程(子模块) api_gateway_service 的 pom.xml 中导入 filter 限流 redis 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_demo</artifactId>
<groupId>djh.it</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway_service</artifactId>
<dependencies>
<!-- springcloudgateway 的内部是通过 netty + webflux 实现。
webflux 实现和 springmvc 存在冲突,需要注销掉父工程中的 web 依赖,在各子模块中导入 web 依赖。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 引入 EurekaClient 依赖坐标 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置 SCG 网关 filter 限流 依赖坐标:redis 监控依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 配置 SCG 网关 filter 限流 依赖坐标:redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
</dependencies>
</project>
<!-- spring_cloud_demo\api_gateway_service\pom.xml -->
4、在子工程 api_gateway_service(子模块)的配置文件 application.yml 中,添加 filter 限流配置。
## spring_cloud_demo\api_gateway_service\src\main\resources\application.yml
server:
port: 8088 # 启动端口 命令行注入。
spring:
application:
name: api-gateway-service #spring应用名, # 注意 FeignClient 不支持名字带下划线
redis: # 引入 redis
host: localhost
pool: 6379
database: 0
# 配置 SpringCloudGateway 的路由
cloud:
gateway:
routes: # 配置路由,路由Id,路由到微服务的 uri, 断言(判断条件)
- id: product-service # 保持唯一
# uri: http://127.0.0.1:9001 # 目标微服务请求地址
uri: lb://service-product # lb:// 根据微服务名称从注册中心拉取服务请求路径。
predicates: # 断言(判断条件)设置
# - Path=/product/** # 路由条件 path : 路由路径匹配条件。 # 即:http://localhost:8088/product/1 相当于 http://127.0.0.1:9001/product/1
- Path=/product-service/** # 将当前请求转发到 http://127.0.0.1/product/1
filters: # 配置路由过滤器 http://localhost:8088/product-service/product/1 --> http://127.0.0.1:9001/product/1
- name: RequestRateLimiter # 使用限流过滤器,是 springcloudgateway 提供的
args:
key-resolver: '#{@pathKeyResolver}' # 使用 SqEL 从容器中获取对象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的上限
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器。
# # 在 gateway 下面 配置自动的根据微服务名称进行路由转发 http://localhost:8088/service-product/product/1
# discovery:
# locator:
# enabled: true # 开启根据服务名称自动转发
# lower-case-service-id: true # 微服务名称已小写形式呈现。
# - After=xxxxxx # 路由断言之后匹配。
# - Before=xxxxxx # 路由断言之前匹配。
# - Between=xxxx.xxxx # 路由断言之间匹配。
# - Cookie=chocolate, ch.p # 路由断言 cookie 匹配,此 predicate 匹配给定名称(chocolate)和正则表达式(ch.p)
# - Header=X-Request-Id, /d+ # 路由断言 Header 匹配,header 名称匹配 X-Request-Id,且正则表达式匹配 /d+
# - Host=**.somehost.org, **.anotherhost.org # 路由断言 Host 匹配,匹配 host 主机列表, ** 代表可变参数。
# - Method=GET # 路由断言 Method 匹配,匹配的是请求的 HTTP 方法。
# # - Path=/foo/{segment},/bar/{segment} # 路由断言匹配,{segment} 为可变参数。
# - Query=baz # 或 Query=foo,ba. # 路由断言 Query 匹配,将请求的参数 param(bax)进行匹配,也可以进行 regexp 正则表达式匹配(参数包含 foo, 并且 foo 的值匹配 ba.)。
# - RemoteAddr=192.168.1.1/24 # 路由断言 RemoteAddr 匹配,将匹配 192.168.1.1~192.168.1.254 之间的 ip 地址,其中 24 为子网掩码位。
eureka: # 配置 Eureka
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true # 使用ip地址注册。
5、在子工程 api_gateway_service(子模块)中,创建 自定义限流规则类 KeyResolverConfiguration.java
/**
* spring_cloud_demo\api_gateway_service\src\main\java\djh\it\gateway\KeyResolverConfiguration.java
*
* 2024-5-8 创建 自定义限流规则类 KeyResolverConfiguration.java
*/
package djh.it.gateway;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class KeyResolverConfiguration {
/**
* 编写基于请求路径的限流规则:
* 方法名需与 application.yml 配置文件中一致
*/
@Bean
public KeyResolver pathKeyResolver(){
//自定义的 KeyResolver
return new KeyResolver(){
// ServerWebExchange : 上下文参数
public Mono<String> resolve(ServerWebExchange exchange){
return Mono.just( exchange.getRequest().getPath().toString() );
}
};
}
}
6、在子工程 api_gateway_service(子模块)中,注销掉 自定义全局过滤器类 LoginFilter.java
/**
* spring_cloud_demo\api_gateway_service\src\main\java\djh\it\gateway\filter\LoginFilter.java
*
* 2024-5-7 创建一个自定义全局过滤器类 LoginFilter.java
* 实现 globalfilter, ordered 接口。
*/
package djh.it.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//@Component
public class LoginFilter implements GlobalFilter, Ordered {
/**
* 执行过滤器中的业务逻辑
* 对请求参数中的 access-token 进行判断
* 如果存在此参数 :代表已经认证成功。
* 如果不存在此参数 :代表认证失败。
* ServerWebExchange 对象 : 相当于请求和响应的上下文(类似 zuul 中的 RequestContext 对象)
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行了自定义的全局过滤器");
//1.获取请求参数 access-token
String token = exchange.getRequest().getQueryParams().getFirst("access-token");
//2.判断是否存在 token
if(token == null){
//3.如果不存在 token 代表认证失败,请求结束。
System.out.println("没有登录");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); //请求结束
}
//4.如果存在 token 参数,继续向下执行
return chain.filter(exchange); //继续向下执行
}
/**
* 指定过滤器的执行顺序,返回值越小,执行优先级越高。
*/
public int getOrder() {
return 0;
}
}
7、重新启动 父工程 spring_cloud_demo 下 全部子项目(eureka,product,order,gateway)的启动类,进行测试:
1)浏览器地址栏输入(正常访问):
http://localhost:8088/product-service/product/1 就转发到 http://127.0.0.1:9001/product/1
2)浏览器地址栏输入(多次连击,触发限流,就不能正常访问了):
http://localhost:8088/product-service/product/1 就转发到 http://127.0.0.1:9001/product/1
3)查看 redis-cli.exe 客户端程序,如下:
8、在子工程 api_gateway_service(子模块)中,修改 自定义限流规则类 KeyResolverConfiguration.java 添加 基于请求 参数 的限流规则的方法:
/**
* spring_cloud_demo\api_gateway_service\src\main\java\djh\it\gateway\KeyResolverConfiguration.java
*
* 2024-5-8 创建 自定义限流规则类 KeyResolverConfiguration.java
*/
package djh.it.gateway;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class KeyResolverConfiguration {
/**
* 编写基于请求路径的限流规则:
* 方法名需与 application.yml 配置文件中一致
*/
// @Bean
public KeyResolver pathKeyResolver(){
//自定义的 KeyResolver
return new KeyResolver(){
// ServerWebExchange : 上下文参数
public Mono<String> resolve(ServerWebExchange exchange){
return Mono.just( exchange.getRequest().getPath().toString() );
}
};
}
/**
* 编写 基于请求 参数 的限流规则的方法:
* 注意:多个规则有可能冲突,需注销掉上面规则。
* 请求参数,如:abc ? userId=1
*/
@Bean
public KeyResolver userKeyResolver(){
//自定义的 KeyResolver
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("userId") //基于请求参数
//exchange.getRequest().getHeaders().getFirst("X-Forwarded-For") //基于请求ip
);
}
}
9、在子工程 api_gateway_service(子模块)中,修改 application.yml 配置文件,
添加 key-resolver: ‘#{@userKeyResolver}’ # 使用 SqEL 从容器中获取对象(基于请求 参数 的限流规则)
## spring_cloud_demo\api_gateway_service\src\main\resources\application.yml
server:
port: 8088 # 启动端口 命令行注入。
spring:
application:
name: api-gateway-service #spring应用名, # 注意 FeignClient 不支持名字带下划线
redis: # 引入 redis
host: localhost
pool: 6379
database: 0
# 配置 SpringCloudGateway 的路由
cloud:
gateway:
routes: # 配置路由,路由Id,路由到微服务的 uri, 断言(判断条件)
- id: product-service # 保持唯一
# uri: http://127.0.0.1:9001 # 目标微服务请求地址
uri: lb://service-product # lb:// 根据微服务名称从注册中心拉取服务请求路径。
predicates: # 断言(判断条件)设置
# - Path=/product/** # 路由条件 path : 路由路径匹配条件。 # 即:http://localhost:8088/product/1 相当于 http://127.0.0.1:9001/product/1
- Path=/product-service/** # 将当前请求转发到 http://127.0.0.1/product/1
filters: # 配置路由过滤器 http://localhost:8088/product-service/product/1 --> http://127.0.0.1:9001/product/1
- name: RequestRateLimiter # 使用限流过滤器,是 springcloudgateway 提供的
args:
# key-resolver: '#{@pathKeyResolver}' # 使用 SqEL 从容器中获取对象(基于请求路径的限流规则)
key-resolver: '#{@userKeyResolver}' # 使用 SqEL 从容器中获取对象(基于请求 参数 的限流规则)
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的上限
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器。
# # 在 gateway 下面 配置自动的根据微服务名称进行路由转发 http://localhost:8088/service-product/product/1
# discovery:
# locator:
# enabled: true # 开启根据服务名称自动转发
# lower-case-service-id: true # 微服务名称已小写形式呈现。
# - After=xxxxxx # 路由断言之后匹配。
# - Before=xxxxxx # 路由断言之前匹配。
# - Between=xxxx.xxxx # 路由断言之间匹配。
# - Cookie=chocolate, ch.p # 路由断言 cookie 匹配,此 predicate 匹配给定名称(chocolate)和正则表达式(ch.p)
# - Header=X-Request-Id, /d+ # 路由断言 Header 匹配,header 名称匹配 X-Request-Id,且正则表达式匹配 /d+
# - Host=**.somehost.org, **.anotherhost.org # 路由断言 Host 匹配,匹配 host 主机列表, ** 代表可变参数。
# - Method=GET # 路由断言 Method 匹配,匹配的是请求的 HTTP 方法。
# # - Path=/foo/{segment},/bar/{segment} # 路由断言匹配,{segment} 为可变参数。
# - Query=baz # 或 Query=foo,ba. # 路由断言 Query 匹配,将请求的参数 param(bax)进行匹配,也可以进行 regexp 正则表达式匹配(参数包含 foo, 并且 foo 的值匹配 ba.)。
# - RemoteAddr=192.168.1.1/24 # 路由断言 RemoteAddr 匹配,将匹配 192.168.1.1~192.168.1.254 之间的 ip 地址,其中 24 为子网掩码位。
eureka: # 配置 Eureka
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true # 使用ip地址注册。
10、重新启动 父工程 spring_cloud_demo 下 全部子项目(eureka,product,order,gateway)的启动类,进行测试:
1)浏览器地址栏输入(无参数,不能正常访问):
http://localhost:8088/product-service/product/1 就转发到 http://127.0.0.1:9001/product/1
2)浏览器地址栏输入(带参数,正常访问):
http://localhost:8088/product-service/product/1?userId=1 就转发到 http://127.0.0.1:9001/product/1
3)浏览器地址栏输入(带参数访问,多次连击,触发限流,就不能正常访问了):
http://localhost:8088/product-service/product/1?userId=1 就转发到 http://127.0.0.1:9001/product/1
4)查看 redis-cli.exe 客户端程序,如下:
上一节关联链接请点击
# 从浅入深 学习 SpringCloud 微服务架构(十一)–SpringCloudGateWay(2)