Apache ShenYu 网关 ratelimiter 插件应用以及源码分析

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-->
插件测试

当配置好选择器以及规则参数后,我们就能通过测试来看到限流效果。

测试数据
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 }

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注