Soul网关源码学习(17)- 限流:RateLimiterPlugin

前言

在上一篇文章《Soul网关源码学习(16)- Resilience4JPlugin分析》中,我们学习了 Soul 是如何集成 Resilience4J 框架的,这样熔断的三个插件里面就只剩下 SentinelPlugin 没有分析,但是后来发现 Sentinal 的集成代码非常的简单,有了前面两篇的参考之后,我相信小伙伴们自己去看难道也不大,所以决定这部分就不再过多分析了。接下来,我们再来学习一个新的插件 RateLimiterPlugin。

RateLimiterPlugin 的使用

RateLimiterPlugin 是网关流量管控限制核心的实现,可以到接口级别,也可以到参数级别,具体怎么用,还得看你对流量配置。
和其他插件一样要使用 RateLimiterPlugin 首先需要在网关引入相关的依赖:

<dependency>
    <groupId>org.dromara</groupId>
    <artifactId>soul-spring-boot-starter-plugin-ratelimiter</artifactId>
    <version>${project.version}</version>
</dependency>

然后登录控制台 -> 系统管理 -> 插件,在插件列表中选择 rate_limiter,点击最右边的 Editor 按钮,在弹出的界面中配置插件信息,RateLimiterPlugin 必须要配置 redis 连接信息:

  • 目前支持redis的单机,哨兵,以及集群模式。
  • 如果是哨兵,集群等多节点的,在URL中的配置,请对每个实列使用 ; 分割. 如 192.168.1.1:6379;192.168.1.2:6379。
    在这里插入图片描述
    添加选择器规则,控制台 -> 插件列表 -> rate_limiter -> 添加选择器按钮:
    在这里插入图片描述

为接口添加限流规则:控制台 -> 插件列表 -> rate_limiter -> 添加规则按钮:
在这里插入图片描述

最后点击一下 同步自定义规则按钮,让新配置的信息同步到网关,这样限流配置就起作用了,下面我们来测试一下:

# 发送100个请求,并发数是100
ab -n100 -c100 localhost:9195/http/order/findById?id=1
#下面是返回的部分指标
Concurrency Level:      100
Time taken for tests:   9.497 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13000 bytes
HTML transferred:       4000 bytes
# 这是关键指标,QPS 接近 10 和上面的配置一样
Requests per second:    10.53 [#/sec] (mean)

通过上面的测试结果,证明了我们的演示已经成功了,接下来我们来分析一下它的实现原理。

RateLimiterPlugin 的实现原理

RateLimiterPlugin 采用redis令牌桶算法进行限流,这是官方的流程图:
在这里插入图片描述
接下我们最主要的是分析一下其代码的实现,我们使用倒推的方法,先直接跳到 RateLimiterPlugin#doExecute 方法:

@Override
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()) {...}
                //如果允许则通过
                return chain.execute(exchange);
            });
}

通过上面代码知道, 方法 redisRateLimiter#isAllowed 是其实现的核心:

public Mono<RateLimiterResponse> isAllowed(final String id, final double replenishRate, final double burstCapacity) {
   ...
   //我们先暂时忽略 redis 令牌 获取的逻辑,待会下面分析,我们直接关心返回结果
   //返回结果有两个 singletonList,第一个是允许通过的请求数,第二个是剩余的令牌数
   Flux<List<Long>> resultFlux = Singleton.INST.get(ReactiveRedisTemplate.class).execute(this.script, keys, scriptArgs);
   return resultFlux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
   		//把两个 singletonList 合并为一个List
           .reduce(new ArrayList<Long>(), (longs, l) -> {
               longs.addAll(l);
               return longs;
           }).map(results -> {
           		//允许通过的请求数是否 == 1
           		//这里就是上面 RateLimiterPlugin#doExecute 中 if (!response.isAllowed())  判断的来源
               boolean allowed = results.get(0) == 1L;
               	//剩余的令牌数量
               Long tokensLeft = results.get(1);
               RateLimiterResponse rateLimiterResponse = new RateLimiterResponse(allowed, tokensLeft);
               return rateLimiterResponse;
           }).doOnError(throwable -> log.error("Error determining if user allowed from redis:{}", throwable.getMessage()));
}

现在我们已经知道了 redisRateLimiter 是如何通过 redis client 请求令牌的,并且获取到请求令牌结果后又是如何处理的。那令牌又是如何生成的呢?我们在 RedisRateLimiter#redisScript 方法中找到了其 lua 脚本(这脚本可以):

--tokens_key 的格式: request_rate_limiter.{id}.tokens,其中 id 是你上面控制台配置的规则的数据库ID,说明令牌桶的粒度可以到接口,甚至带参数级别
local tokens_key = KEYS[1]
--timestamp_key 格式: request_rate_limiter.{id}.timestamp
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
--redis操作的超时时间,2陪的填充时间
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))
if last_refreshed == nil then
 last_refreshed = 0
end
-- 距离上次刷新令牌的时间间隔
local delta = math.max(0, now-last_refreshed)
--计算刷新后的令牌数
--last_tokens+(delta*rate):上次剩余 + 时间 * 刷新速率
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
-- 如果令牌数大于请求数则通过
-- soul 是默认每次令牌请求数都是1,不会批量获取
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)
-- 返回允许通过的请求数和剩余令牌数,这就是 redisRateLimiter#isAllowed 方法中提到的两个 singletonList
return { allowed_num, new_tokens }

通过上面脚本的注释,我相信小伙伴都能弄明白其令牌获取的逻辑。这样我们就通过倒推的方式从方法 RateLimiterPlugin#doExecute 开始,再到 redisRateLimiter#isAllowed 方法(通过 redis client 获取令牌),最后到 lua 脚本(原子性刷新令牌桶并且返回令牌获取结果),从而理清楚了其整个限流的实现脉络,其中令牌桶的实现,我们以后碰到同样的需要的时候,也可以参考这里的实现。

总结

这一章,我们先简单示范了一下 soul 网关应该如何配置和使用限流插件,然后对限流插件的设计方案做了简单的讲解,最后通过反推的方式重点分析了限流插件的实现原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值