ratelimiter插件
Apache ShenYu 网关 ratelimiter插件是采用redis令牌桶算法来对接口限流,支持接口颗粒度的配置。
插件设置
- shenyu-admin -> 插件管理 -> ratelimiter编辑 -> 开启
- shenyu-bootstrap中加入相关依赖,并重启项目
<!-- shenyu ratelimiter plugin start-->
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-spring-boot-starter-plugin-ratelimiter</artifactId>
<version>${last.version}</version>
</dependency>
<!-- shenyu ratelimiter plugin end-->
- 选择器配置请参考Apache ShenYu 网关选择器和规则解析
- ratelimiter插件独有规则配置说明:
- capacity: redis令牌桶总数
- rate: 令牌桶每次增加令牌基数
插件测试
当配置好选择器以及规则参数后,我们就能通过测试来看到限流效果。
测试数据
12线程、400并发,持续 10秒, capacity=1000,rate=1000
wrk -t12 -c400 -d10s http://localhost:9195/http/order/findById\?id\=10
Running 10s test @ http://localhost:9195/http/order/findById?id=10
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 81.10ms 78.39ms 622.93ms 87.58%
Req/Sec 272.77 160.33 787.00 60.42%
32291 requests in 10.06s, 5.24MB read
Socket errors: connect 158, read 289, write 4, timeout 0
Non-2xx or 3xx responses: 31272
Requests/sec: 3208.59
Transfer/sec: 533.01KB
请求日志:
tokensRemaining : 令牌桶剩余令牌
allowed: 是否允许
2021-02-01 16:33:59.310 INFO 61415 --- [ioEventLoop-6-1] o.d.s.p.r.executor.RedisRateLimiter : RateLimiter response:Response{allowed=true, tokensRemaining=264}
2021-02-01 16:33:59.429 INFO 61415 --- [ioEventLoop-6-1] o.d.s.p.r.executor.RedisRateLimiter : RateLimiter response:Response{allowed=false, tokensRemaining=0}
源码分析
ratelimiter插件 采用的是redis令牌桶算法来对接口限流。它的核心算法是通过redis lua脚本完成的。它的核心逻辑是,每次请求的时候通过lua脚本从redis取令牌桶令牌剩余数量以及最新更新时间,如果令牌桶里的令牌数量大于等于请求数,则请求成功,并通过lua脚本更新最新的令牌数量。令牌桶补令牌是通过我们最新令牌数 + rate参数 x (now - 最新更新时间)。详情见下面 lua 脚本源码分析。
ratelimiter插件流程图(源自soul官网)
核心源码:
public class RedisRateLimiter {
public Mono<RateLimiterResponse> isAllowed(final String id, final double replenishRate, final double burstCapacity) {
……
//拼接 redis lua脚本参数
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
//执行 redis lua脚本, this.script就是lua脚本,具体下面会分析
Flux<List<Long>> resultFlux = Singleton.INST.get(ReactiveRedisTemplate.class).execute(this.script, keys, scriptArgs);
return resultFlux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
.reduce(new ArrayList<Long>(), (longs, l) -> {
longs.addAll(l);
return longs;
}).map(results -> {
// lua脚本中取出是否允许和令牌桶剩余token值
boolean allowed = results.get(0) == 1L;
Long tokensLeft = results.get(1);
RateLimiterResponse rateLimiterResponse = new RateLimiterResponse(allowed, tokensLeft);
log.info("RateLimiter response:{}", rateLimiterResponse.toString());
return rateLimiterResponse;
……
}
}
lua脚本核心源码分析:
源码在 shenyu-plugin-ratelimiter 项目 resources 下的 /META-INF/scripts/request_rate_limiter.lua
-- 根据请求的时间在redis里获取最新的token,如果不存在,则将请求传来的当前最新的值赋值给last_tokens
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
……
-- 根据传递过来的时间戳key,在redis中获取最近的刷新时间,如果为NUll,则赋值为0
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
……
-- 用当前时间减上一次更新时间,来得到本次新增令牌数量 rate的乘数
local delta = math.max(0, now-last_refreshed)
-- 根据传递的参数补充令牌桶数量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
-- 如果当前最新的令牌桶数量大于等于请求数量则返回true,表示通过
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
-- 如果当前最新的令牌桶数量大于等于请求数量
if allowed then
-- 更新最新令牌数
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 将最新值令牌桶数量以及最近更新时间戳更新到redis(此操作是原子性的)
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
-- 返回结果
return { allowed_num, new_tokens }