为什么要限流
项目上线后,由于每个服务器的处理效率和消耗资源并不是无限的。当指定时间段内请求过高,会导致服务资源吃不消,造成雪崩等问题。
常见的限流算法
技术器算法
做限流 (Rate Limiting/Throttling) 的时候,除了简单的控制并发,如果要准确的控制 TPS,简单的做法是维护一个单位时间内的 Counter,如判断单位时间已经过去,则将 Counter 重置零。
但该算法有个很致命的问题:
此做法被认为没有很好的处理单位时间的边界。
比如在前一秒的最后一秒里
和下一秒的第一秒
都触发了最大
的请求数,也就是在两秒内
发生了两倍的 TPS
。
其次,最后1s和最初1s请求吃满,在指定的时间段内,其他区间时间上则存在了资源浪费问题。
漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。
其底层采取队列处理的:
但也存在一些资源消耗问题(突发请求处理
)。
1、大量请求达到网关时。由于请求处理速率平稳,导致大量请求堆积至网关中,造成网关资源浪费,压力过大。
2、请求堆积过多,导致大量的请求将会被丢弃。
3、浪费微服务的处理,比如设定1s一个请求,但一般的微服务每秒的处理效率可达几百几千,严重浪费资源。
令牌桶算法
令牌桶算法是漏桶算法的一种改进。
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。
随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。
新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。
其流程概述如下所示:
1、假定令牌桶的容量是10个。
2、恒定速率生成令牌,生成令牌后,会判断令牌桶中的大小,如果没有容量了,则令牌丢弃;否则就保存至桶中。
3、正常来说每次请求过来时,都会从令牌桶中拿到一个令牌。
4、如果令牌拿完,并且此时令牌还在生成中,则将后续请求进行丢弃或者放入队列中缓存。
项目搭建测试
gateway RequestRateLimiter 官方文档
依赖引入
<!--引入redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!--对象池 redis高版本中未使用jedis,采取的netty非阻塞型连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件编写
server:
port: 10000 # gateway的port
spring:
application:
name: gateway-server # 应用服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册地址
gateway:
routes:
- id: nacos-product # 微服务别名称
uri: lb://nacos-product # lb://服务别名 根据服务名称从注册中心获取服务ip+port信息,lb:// 表示支持负载均衡
predicates: # 断言
- Path=/product/** #path规则,匹配对应的URL请求,将匹配到的请求追加至目标URI之后
filters: # 网关过滤器
# 限流过滤器
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率,即行等价于允许用户每秒处理多少个请求平均数
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 2
# 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
key-resolver: "#{@pathKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称
redis:
database: 0
host: 192.168.99.100
port: 10000
password: linkpower
timeout: 10000 #连接超时时间
lettuce:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 300
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1s
#连接池中的最大空闲连接
max-idle: 100
#连接池中的最小空闲连接
min-idle: 20
请求路径限流
package cn.linkpower.config;
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 MyResolverConf {
/**
* 限流规则<br/>
* pathKeyResolver 必须和application.yml文件中的 #{@pathKeyResolver} 保持一致
* @return
*/
@Bean
public KeyResolver pathKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
}
请求测试:
http://localhost:10000/product/getProduct/5
参数限流
相对于上面的配置文件,只需要修改yml
和java 配置类
中对应的配置即可,配置如下所示:
server:
port: 10000 # gateway的port
spring:
application:
name: gateway-server # 应用服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册地址
gateway:
routes:
- id: nacos-product # 微服务别名称
uri: lb://nacos-product # lb://服务别名 根据服务名称从注册中心获取服务ip+port信息,lb:// 表示支持负载均衡
predicates: # 断言
- Path=/product/** #path规则,匹配对应的URL请求,将匹配到的请求追加至目标URI之后
filters: # 网关过滤器
# 限流过滤器
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率,即行等价于允许用户每秒处理多少个请求平均数
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 2
# 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
#key-resolver: "#{@pathKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称 ——请求路径限流
key-resolver: "#{@paramKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称 ——参数限流
redis:
database: 0
host: 192.168.99.100
port: 10000
password: linkpower
timeout: 10000 #连接超时时间
lettuce:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 300
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1s
#连接池中的最大空闲连接
max-idle: 100
#连接池中的最小空闲连接
min-idle: 20
# redis:
# database: 0
# host: 192.168.99.100
# port: 10000
# password: linkpower
# timeout: 10000 #连接超时时间
# jedis: ## jedis配置
# pool: ## 连接池配置
# max-idle: 8 # 最大空闲连接数
# max-active: 8 # 最大连接数
# max-wait: 3000 # 最大阻塞等待时间
# min-idle: 0 # 最小空闲连接数
package cn.linkpower.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class MyResolverConf {
/**
* 限流规则<br/>
* pathKeyResolver 必须和application.yml文件中的 #{@pathKeyResolver} 保持一致
* @return
*/
//@Bean
// @Primary //添加这个注解是为了防止多个限流规则注册bean报错
public KeyResolver pathKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
/**
* 参数限流
* @return
*/
@Bean
public KeyResolver paramKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
// 随便指明一个参数
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userid"));
}
}
此时请求测试,必须携带参数信息,因为在yml中针对该api追加了参数限流:
如果不添加参数请求时,如下所示:
http://localhost:10000/product/getProduct/5
增加参数,分别定义不同的参数请求:
http://localhost:10000/product/getProduct/5
?userid=1
也会出现限流操作。
不过是针对不同的参数值,设定了不同的令牌桶。
IP限流
ip限流的配置也和上述一样,只需要修改对应的yml和java配置类即可。
server:
port: 10000 # gateway的port
spring:
application:
name: gateway-server # 应用服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册地址
gateway:
routes:
- id: nacos-product # 微服务别名称
uri: lb://nacos-product # lb://服务别名 根据服务名称从注册中心获取服务ip+port信息,lb:// 表示支持负载均衡
predicates: # 断言
- Path=/product/** #path规则,匹配对应的URL请求,将匹配到的请求追加至目标URI之后
filters: # 网关过滤器
# 限流过滤器
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率,即行等价于允许用户每秒处理多少个请求平均数
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 2
# 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
#key-resolver: "#{@pathKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称 ——请求路径限流
#key-resolver: "#{@paramKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称 ——参数限流
key-resolver: "#{@ipKeyResolver}" # pathKeyResolver是自己配置项目的bean的名称 ——ip限流
redis:
database: 0
host: 192.168.99.100
port: 10000
password: linkpower
timeout: 10000 #连接超时时间
lettuce:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 300
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1s
#连接池中的最大空闲连接
max-idle: 100
#连接池中的最小空闲连接
min-idle: 20
# redis:
# database: 0
# host: 192.168.99.100
# port: 10000
# password: linkpower
# timeout: 10000 #连接超时时间
# jedis: ## jedis配置
# pool: ## 连接池配置
# max-idle: 8 # 最大空闲连接数
# max-active: 8 # 最大连接数
# max-wait: 3000 # 最大阻塞等待时间
# min-idle: 0 # 最小空闲连接数
package cn.linkpower.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class MyResolverConf {
/**
* 限流规则<br/>
* pathKeyResolver 必须和application.yml文件中的 #{@pathKeyResolver} 保持一致
* @return
*/
// @Bean
// @Primary //添加这个注解是为了防止多个限流规则注册bean报错
public KeyResolver pathKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
/**
* 参数限流
* @return
*/
//@Bean
public KeyResolver paramKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
// 随便指明一个参数
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userid"));
}
@Bean
public KeyResolver ipKeyResolver(){
// return new KeyResolver() {
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getPath().toString());
// }
// }
// 随便指明一个参数
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}