Soul网关源码探秘《十九》 - RateLimiter 插件源码分析

前文探讨的 Hystrix 插件可以用来实现熔断降级。今天来研究可以实现限流的 RateLimiter 插件。

准备工作

  1. 启动soul-admin项目,并在后台页面开启divide和RateLimiter插件。

开启RateLimiter插件时需要配置redis信息。在本地起一个redis服务。
RateLimiter配置
2. 在soul-admin中配置RateLimiter插件的选择器和规则。

选择器:
选择器的配置
规则:

规则配置
选择器的配置跟之前探讨过的插件都类似。规则配置中有两个差异化的参数。

  • capacity:是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以保存的令牌数
  • rate:是你允许用户每秒执行多少请求,而丢弃任何请求。这是令牌桶的填充速率
  1. 在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项目。

  1. 启动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已经生效了,并且拒绝掉了一些请求。
RateLimiter生效

源码分析

查看soul官网,有介绍RateLimiter的令牌桶流程。

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 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rughru

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值