Soul 源码阅读系列之限流器、RewritePlugin、ContextPathPlugin

本文详细介绍了Soul网关如何利用Redis实现限流器,通过RateLimiterPluginDataHandler初始化Redis连接,并使用lua脚本进行限流计算。此外,还讲解了RewritePlugin和ContextPathMappingPlugin如何实现URL重写和contextPath匹配,为请求处理提供灵活的路径配置。
摘要由CSDN通过智能技术生成

ratelimiter

soul 网关使用 redis 实现了一个限流器。

RateLimiterPluginDataHandler

当插件配置被修改时(位于 admin 系统->系统管理->插件管理),数据将流向此类的 handlerPlugin 方法,soul 使用这些信息组装 redis 连接:

public void handlerPlugin(final PluginData pluginData) {
        if (Objects.nonNull(pluginData) && pluginData.getEnabled()) {
            //init redis
            // 获取限流器配置
            RateLimiterConfig rateLimiterConfig = GsonUtils.getInstance().fromJson(pluginData.getConfig(), RateLimiterConfig.class);
            //spring data redisTemplate
            if (Objects.isNull(Singleton.INST.get(ReactiveRedisTemplate.class))
                    || Objects.isNull(Singleton.INST.get(RateLimiterConfig.class))
                    || !rateLimiterConfig.equals(Singleton.INST.get(RateLimiterConfig.class))) {
                // 使用配置生成 Lettuce 连接工厂实例
                LettuceConnectionFactory lettuceConnectionFactory = createLettuceConnectionFactory(rateLimiterConfig);
                lettuceConnectionFactory.afterPropertiesSet();
                RedisSerializer<String> serializer = new StringRedisSerializer();
                RedisSerializationContext<String, String> serializationContext =
                        RedisSerializationContext.<String, String>newSerializationContext().key(serializer).value(serializer).hashKey(serializer).hashValue(serializer).build();
                // 生成 redis 连接实例并放入缓存
                ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new SoulReactiveRedisTemplate<>(lettuceConnectionFactory, serializationContext);
                Singleton.INST.single(ReactiveRedisTemplate.class, reactiveRedisTemplate);
                Singleton.INST.single(RateLimiterConfig.class, rateLimiterConfig);
            }
        }
    }

RedisRateLimiter

RedisRateLimiter 类维护了实现限流功能的关键 lua 脚本:

    private RedisScript<List<Long>> redisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        // 加载 lua 脚本,放入 java 内存
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/META-INF/scripts/request_rate_limiter.lua")));
        redisScript.setResultType(List.class);
        return redisScript;
    }

脚本关键逻辑如下:

...

// 获取上一次的数量
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.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

同时使用 isAllowed 方法封装了 lua 脚本的调用过程:

    public Mono<RateLimiterResponse> isAllowed(final String id, final double replenishRate, final double burstCapacity) {
        ... 
        // 从 Singleton 中获取 RateLimiterPluginDataHandler 类缓存的 ReactiveRedisTemplate
        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 -> {
                    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;
                }).doOnError(throwable -> log.error("Error determining if user allowed from redis:{}", throwable.getMessage()));
    }

RateLimiterPlugin

最终 RateLimiterPlugin 的 doExecute 方法只需要调用 RedisRateLimiter 实例的 isAllowed 方法即可。

RewritePlugin

RewritePlugin 提供了 url 的重写功能,代码比较简单,将用户配置的 rewriteURI 写入上下文:

    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        ...
        // 设置 RewriteURI
        exchange.getAttributes().put(Constants.REWRITE_URI, rewriteHandle.getRewriteURI());
        return chain.execute(exchange);
    }

然后在 DividePlugin 的逻辑中,会在组装实际 URL 的时候判断此参数:

    private String buildRealURL(final String domain, final SoulContext soulContext, final ServerWebExchange exchange) {
        String path = domain;
        final String rewriteURI = (String) exchange.getAttributes().get(Constants.REWRITE_URI);
        // 如果设置了 rewriteURI,则以 rewriteURI 为准
        if (StringUtils.isNoneBlank(rewriteURI)) {
            path = path + rewriteURI;
        } else {
            final String realUrl = soulContext.getRealUrl();
            if (StringUtils.isNoneBlank(realUrl)) {
                path = path + realUrl;
            }
        }
        ...
        return path;
    }

ContextPathMappingPlugin

ContextPathMappingPlugin 提供了 contextPath 的重写功能,比如请求路径为/soul/http/order, 配置的contextPath为’/soul/http’,那么真正请求的url为’/order’。
代码也相对简单,进行匹配替换即可:

    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        ...
        //check the context path illegal
        // 如果 contextPath 并非以用户设置的替换规则开始,则报错
        if (!soulContext.getPath().startsWith(contextMappingHandle.getContextPath())) {
            Object error = SoulResultWrap.error(SoulResultEnum.CONTEXT_PATH_ERROR.getCode(), SoulResultEnum.CONTEXT_PATH_ERROR.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        // 替换路径 url
        this.buildContextPath(soulContext, contextMappingHandle);
        return chain.execute(exchange);
    }
    
    private void buildContextPath(final SoulContext context, final ContextMappingHandle handle) {
        ...
        // 分词
        Optional<String> optional = Arrays.stream(context.getPath()
                .split(handle.getContextPath()))
                .reduce((first, last) -> last);
        // 设置 RealUrl
        optional.ifPresent(context::setRealUrl);
    }

最终,会在上文提到的 DividePlugin 插件中,以低于 rewrite 插件配置的优先级进行 URL 组装。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值