限流一般是一种保护系统稳定性的一种措施。在开放API中比较常见。
对某一用户,或指定IP,或具体到某个API,在一个具体的时间里限制请求次数。
关键词
- 熔断
- 服务降级
- 延迟处理
- 特权处理
首先看看主要的框架是使用,再讨论自己怎么实现
Nginx 自带的两个限流模块
- ngx_http_limit_conn_module 连接数限流模块
- ngx_http_limit_req_module 请求数限流模块
- ngx_lua
- ngx_lua_waf模块
- OpenResty
ngx_http_limit_conn_module
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;
...
server {
...
location /download/ {
limit_conn addr 1;
}
ab test
ab -n10 -c3 https://ifuture.pro/test/
- limit_conn_zone: 配置限流的key以及存储这些key共用的共享内存的大小;
- key 是$binary_remote_addr,表示IP地址,如果如果需要对总域名进行限流,key就应该使用 $server_name $host等等,能唯一表示该域名的Nginx变量;
- zone=addr:10m中,addr表示连接数限流的区域名称,10m表示可以分配的共享空间的大小。
- binary_remote_addr变量在64位平台中占用64字节。1M共享空间可以保存1.6万个64位的,10m就可以保存16万个。如果超过16万个,共享空间被用完,服务器将会对后续所有的请求返回 503。
- limit_conn:配置指定key的最大连接数。样例中指定的最大连接数是1,表示Nginx最多同时允许1个连接进行location /limit 的行为操作。
- limit_conn_status:配置被限流后返回的状态码,样例中设置的是503.
- limit_conn_log_level:配置被限流后的日志级别,设置的是error级别
ngx_http_limit_req_module
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
limit_req_log_level error;
limit_req_status 503;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
}
rate=1r/s 固定请求速率设置,每秒1个请求。
http{
...
limit_req_zone $cookie_token zone=session_limit:3m rate=1r/s;
limit_req_zone $binary_remote_addr $uri zone=auth_limit:3m rate=1r/m;
}
location /{
limit_req zone=session_limit burst=5;
rewrite_by_lua '
local random = ngx.var.cookie_random
if (random == nil) then
return ngx.redirect("/auth?url=" .. ngx.var.request_uri)
end
local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random)
if (ngx.var.cookie_token ~= token) then
return ngx.redirect("/auth?url=".. ngx.var.request_uri)
end
';
}
location /auth {
limit_req zone=auth_limit burst=1;
if ($arg_url = "")
{
return403;
}
access_by_lua '
local random = math.random(9999)
local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random)
if (ngx.var.cookie_token ~= token) then
ngx.header["Set-Cookie"] = {
"token=" .. token, "random=" .. random
}
return ngx.redirect(ngx.var.arg_url)
end
';
}
maven pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
appliaction.yml
spring:
cloud:
gateway:
routes:
- id: zzz-transfer
uri: lb://zzz-transfer
order: 8002
predicates:
- Path=/api/tengine/**
- CacheBody={*}
filters:
- StripPrefix=2
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求
redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数
key-resolver: "#{@hostNameKeyResolver}" #SPEL表达式去的对应的bean
enabled: true
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
/**
* IP 限流
* @return
*/
@Bean
@Primary
KeyResolver hostNameKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
/**
* API 地址限流
* @return
*/
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
Spring Cloud Netflix 诸多组件不在更新维护后 Spring Cloud也正式加入了spring-cloud-alibaba Sentinel就是其中一员,它把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
-
基于sentinel-dashboard 界面化配置
下载 sentinel-dashboard https://github.com/alibaba/Sentinel/releases
java -jar sentinel-dashboard.jar --server.port=8080 可能在sentinel dashboard上不显示,请求一下个个服务试试
application.yml
spring: application: name: zzz-gateway cloud: nacos: discovery: server-addr: z.com:8848 sentinel: transport: dashboard: z.com:8080 enabled: true
-
基于nacos config
application.yml
spring: application: name: zzz-gateway cloud: nacos: discovery: server-addr: ifuture.pro:8848 config: server-addr: ifuture.pro:8848 file-extension: yaml sentinel: transport: dashboard: ifuture.pro:8080 datasource: ds: nacos: server-addr: ifuture.pro:8848 dataId: spring-cloud-sentinel-nacos groupId: DEFAULT_GROUP rule-type: flow namespace: zzzgateway
- resource: "/api" limitApp: default grade: 1 count: 1 strategy: 0 controlBehavior: 0 clusterMode: true resource:资源名,即限流规则的作用对象。 limitApp:流控针对的调用来源,若为 default 则不区分调用来源。 grade:限流阈值类型,QPS 或线程数模式,0代表根据并发数量来限流,1代表根据QPS来进行流量控制。 count:限流阈值 strategy:判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 controlBehavior:流控效果(直接拒绝 / 排队等待 / 慢启动模式) clusterMode:是否为集群模式
-
基于代码 https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
系统维护一个计数器,来一个请求就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。
基于这个简单的方法,可以再延伸出一些高级功能,比如阈值可以不是固定值,是动态调整的。另外,还可以有多组计数器分别管理不同的服务,以保证互不影响等。
就是基于FIFO队列,所有请求都进入队列,后端程序从队列中取出待处理的请求依次处理。
基于队列的方法,也可以延伸出更多的玩法来,比如可以设置多个队列以配置不同的优先级。
首先还是要基于一个队列,请求放到队列里面。但除了队列以外,还要设置一个令牌桶,另外有一个脚本以持续恒定的速度往令牌桶里面放令牌,后端处理程序每处理一个请求就必须从桶里拿出一个令牌,如果令牌拿完了,那就不能处理请求了。我们可以控制脚本放令牌的速度来达到控制后端处理的速度,以实现动态流控。
- 一个固定容量的桶。如:每分钟能请求的最大量
- 一个永续线程以匀速的方式将令牌放入桶中,超过容量丢弃
- 有请求了,将桶内令牌取出,如果没有足够的令牌则限流
Spring Cloud Gateway
就是基于 Redis + Lua
的方式实现限流的,主要是减少网络开销,使用Lua脚本,无需向Redis 发送多次请求,执行一次即可。
Lua 脚本位置
spring-cloud-gateway-core-2.1.0.RELEASE.jar/META-INF/scripts/request_rate_limiter.lua
可以使用DefaultRedisScript
去调用。模仿spring cloud gateway 自己写一个限流吧
String luaScript = getLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);