soul网关学习十一之RateLimitPlugin插件的调用
作者:互联网
RateLimitPlugin 学习
1.soul-bootstrap 配置
引用插件
2.soul-admin 端开启插件
因为 RateLimitPlugin 流控是使用 redis 的,所以 RateLimitPlugin的配置如下
url 为redis的访问地址,如果是集群多个配置需要使用 “;” 分隔。
配置限流策略
依次配置 RateLimitPlugin 的selector和rules, 我们这里配置对/http/test/findByUserId 进行限流
3.令牌桶算法
soul的RateLimitPlugin 插件是令牌桶算法进行限流的。令牌桶算法是保证一个桶内有一定数量的令牌,并且按照固定速率向这个桶内存放令牌,但是桶内的令牌数量不超过限定值,如果超过则丢弃部分令牌。每次请求都会在这个桶内获取令牌,如果获取到了则继续处理该请求,否则拒绝该请求。这个限流就是在一个时间窗口内限制了请求的最大数量。上图中配置桶的大小为 5 ,存放令牌的速率为每秒1个。
4.RateLimiterPlugin插件对请求处理
插件内使用redis作为"桶"来存放、获取令牌,并且使用 lua 脚本操作来保证一致性
RateLimiterPlugin 插件在 doExecute对请求进行限流处理
RateLimiterPlugin.doExecute
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
final String handle = rule.getHandle();
final RateLimiterHandle limiterHandle = GsonUtils.getInstance().fromJson(handle, RateLimiterHandle.class);
return redisRateLimiter.isAllowed(rule.getId(), limiterHandle.getReplenishRate(), limiterHandle.getBurstCapacity())
.flatMap(response -> {
if (!response.isAllowed()) {
// 如果没有过去到token 返回
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
Object error = SoulResultWrap.error(SoulResultEnum.TOO_MANY_REQUESTS.getCode(), SoulResultEnum.TOO_MANY_REQUESTS.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// 继续执行
return chain.execute(exchange);
});
}
如果获取到令牌则继续执行插件的调用链,否则直接返回错误。
redisRateLimiter.isAllowed
方法调用lua脚本,获取令牌
public Mono<RateLimiterResponse> isAllowed(final String id, final double replenishRate, final double burstCapacity) {
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
}
// redis 中的key,当前rule配置规则对应的token数量key, 当前rule配置规则设置令牌的时间key
List<String> keys = getKeys(id);
// 脚本参数列表(桶中存放token速率, 桶容量,当前时间, 每次请求获取的token数量)
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 使用 ReactiveRedisTemplate 执行 lua 脚本获取token
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 -> {
// 是否获取到 token
boolean allowed = results.get(0) == 1L;
// 剩余token数量
Long tokensLeft = results.get(1);
RateLimiterResponse rateLimiterResponse = new RateLimiterResponse(allowed, tokensLeft);
log.info("RateLimiter response:{}", rateLimiterResponse.toString());
return rateLimiterResponse;
}).doOnError(throwable -> log.error("Error determining if user allowed from redis:{}", throwable.getMessage()));
}
下面看下 lua 脚本是如何获取令牌的。脚本位置:/META-INF/scripts/request_rate_limiter.lua
-- 设置参数
-- 令牌数量 key
local tokens_key = KEYS[1]
-- 获取令牌的时间 key
local timestamp_key = KEYS[2]
-- 令牌生成速率
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)
-- 获取桶中的令牌数量
local last_tokens = tonumber(redis.call("get", tokens_key))
-- 如果没有 则表示第一次获取令牌, 设置令牌数为初始容量
if last_tokens == nil then
last_tokens = capacity
end
-- 获取上次获取令牌的时间
local last_refreshed = tonumber(redis.call("get", timestamp_key))
-- 如果没有 则表示第一次获取令牌, 设上次时间为 0
if last_refreshed == nil then
last_refreshed = 0
end
-- 上次获取token到现在的时间差
local delta = math.max(0, now-last_refreshed)
-- 剩余的 token 数量 = 桶容量与桶中剩余token和时间差内生成的token数量相比小的值
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
-- 如果剩余token数量 >= 本次获取令牌的数量 则获取到令牌
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
-- 获取到token则减少桶中令牌数量
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 更新 令牌数量与获取令牌的时间
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
-- 返回值
return { allowed_num, new_tokens }
如上就是soul如何使用RateLimitPlugin插件进行限流的,如果那里有问题请留言。
标签:网关,令牌,--,插件,soul,tokens,获取,token,local 来源: https://blog.csdn.net/Bo_java/article/details/113576785