前文探讨的 Hystrix 插件可以用来实现熔断降级。今天来研究可以实现限流的 RateLimiter 插件。
准备工作
- 启动soul-admin项目,并在后台页面开启divide和RateLimiter插件。
开启RateLimiter插件时需要配置redis信息。在本地起一个redis服务。
2. 在soul-admin中配置RateLimiter插件的选择器和规则。
选择器:
规则:
选择器的配置跟之前探讨过的插件都类似。规则配置中有两个差异化的参数。
- capacity:是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以保存的令牌数
- rate:是你允许用户每秒执行多少请求,而丢弃任何请求。这是令牌桶的填充速率
- 在soul-bootstrap的pom.xml文件添加插件依赖,并启动网关项目
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-plugin-ratelimiter</artifactId>
<version>${project.version}</version>
</dependency>
启动soul-bootstrap项目以及soul-examples-http项目。
- 启动soul-examples-http项目。
压测
在终端使用以下指令进行压测。
## 50个并发,50个请求,均大于RateLimiter中设置的值,确保插件会拒绝掉大部分请求
wrk -t 50 -c 50 -d 30s http://192.168.31.34:9195/http/order/findById\?id\=5
## 结果
Running 30s test @ http://192.168.31.34:9195/http/order/findById?id=5
50 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 18.93ms 16.52ms 228.59ms 85.77%
Req/Sec 62.31 22.92 181.00 70.97%
93317 requests in 30.10s, 15.28MB read
Non-2xx or 3xx responses: 92908
Requests/sec: 3100.28
Transfer/sec: 519.94KB
检查网关的后台日志,发现RateLimiter已经生效了,并且拒绝掉了一些请求。
源码分析
查看soul官网,有介绍RateLimiter的令牌桶流程。
接收到请求后,先经过RateLimiter插件。如果符合设置的选择器和规则后,会去令牌桶中拿 token。而令牌桶中的 token 是根据设置的速率加入的。所以如果短时间涌入超过设定值的请求,它们在令牌桶中拿不到 token,相应的请求就会直接被拒绝。
直接找到RateLimiter插件中的doExecute方法。
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
// ...
// 处理判断请求是否被允许的逻辑
return redisRateLimiter.isAllowed(rule.getId(), limiterHandle.getReplenishRate(), limiterHandle.getBurstCapacity())
.flatMap(response -> {
// 如果拒绝请求,则直接返回 response
if (!response.isAllowed()) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
// ...
}
return chain.execute(exchange);
});
}
可以看到,判断是否要拒绝请求的关键逻辑在redisRateLimiter.isAllowed
方法中。
public Mono<RateLimiterResponse> isAllowed(final String id, final double replenishRate, final double burstCapacity) {
// ...
// 通过规则 ID 获取 keys
List<String> keys = getKeys(id);
// 装配LUA脚本参数
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 执行LUA脚本
Flux<List<Long>> resultFlux = Singleton.INST.get(ReactiveRedisTemplate.class).execute(this.script, keys, scriptArgs);
// ...
}
发现是通过LUA脚本来维护令牌桶中的token。
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
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.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed_num, new_tokens }