soul网关学习十一之RateLimitPlugin插件的调用

1.soul-bootstrap 配置

引用插件

图片

2.soul-admin 端开启插件

图片

因为 RateLimitPlugin 流控是使用 redis 的,所以 RateLimitPlugin的配置如下

url 为redis的访问地址,如果是集群多个配置需要使用 “;” 分隔。

图片
配置限流策略
依次配置 RateLimitPlugin 的selectorrules, 我们这里配置对/http/test/findByUserId 进行限流

配置selector
配置rule

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插件进行限流的,如果那里有问题请留言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值