SpringCloudGateway源码(四)限流组件

前言

如果不使用Alibaba Sentinel的网关流控规则,

是否可以选择使用SpringCloudGateway基于Redis的限流组件?

基于这个问题,笔者想了解一下scg自带限流组件的实现原理。

一、使用案例

1、pom

注意要加入redis-reactive依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2、KeyResolver

实现一个KeyResolver解析限流资源key,比如针对某个请求路径、针对某个请求路径+用户等。

@Component
public class ExampleKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        String uri = exchange.getRequest().getURI().getPath();
        HttpHeaders headers = exchange.getRequest().getHeaders();
        List<String> keys = headers.get("client_id");
        if (CollectionUtils.isEmpty(keys)) {
            return Mono.just(uri);
        }
        return Mono.just(uri + "_" + keys.get(0));
    }
}

3、路由配置

这里通过编码方式创建一个RouteLocator配置路由。

注:也可以通过RouteDefinitionLocator配置,也可以通过yml配置。

@Bean
public RouteLocator rateLimitRouteLocator(RouteLocatorBuilder builder, ExampleKeyResolver exampleKeyResolver) {
    return builder.routes()
            // curl --header "client_id:aacc" -v localhost:8080/ratelimiter
            .route("test_rate_limit", predicateSpec -> predicateSpec
                    .path("/ratelimiter"// PathRoutePredicate匹配路径
                    .filters(
                            // RedisRateLimiter
                            gatewayFilterSpec -> gatewayFilterSpec.requestRateLimiter().rateLimiter(RedisRateLimiter.class, config -> {
                                config.setReplenishRate(1); // 令牌填充速率
                                config.setBurstCapacity(2); // 桶容量
                                config.setRequestedTokens(1); // 每次请求消耗令牌数量
                            }).configure(config -> {
                                // key解析器
                                config.setKeyResolver(exampleKeyResolver);
                            }))
                    .uri("https://www.aliyun.com")) // 转发uri
            .build();
}

二、原理

1、自动配置

在引入redis-reactive后,开启GatewayRedisAutoConfiguration自动配置。

1)RedisScript:限流lua脚本,脚本位于META-INF/scripts/request_rate_limiter.lua;

2)RedisRateLimiter:基于redis的RateLimiter实现,底层依赖限流lua脚本;

SpringCloudGateway源码(四)限流组件

GatewayAutoConfiguration默认提供了一个KeyResolver的实现PrincipalNameKeyResolver,基于java.security.Principal#getName获取限流资源key,在案例中我们使用ExampleKeyResolver替换了默认实现。

GatewayAutoConfiguration在RateLimiter和KeyResolver存在的情况下,注入RequestRateLimiterGatewayFilterFactory限流过滤器工厂,用于创建限流过滤器。

SpringCloudGateway源码(四)限流组件

2、RequestRateLimiterGatewayFilterFactory

成员变量:

1)defaultRateLimiter:默认全局RateLimiter,如果针对route路由没有定制,默认是RedisRateLimiter;

2)defaultKeyResolver:默认全局KeyResolver,如果针对route路由没有定制,默认是PrincipalNameKeyResolver;

3)denyEmptyKey:是否拒绝KeyResolver解析为空key的请求,默认为true;

4)emptyKeyStatusCode:如果拒绝空key,返回http状态码,默认403Forbidden;

SpringCloudGateway源码(四)限流组件

在ioc容器启动阶段(不同路由配置方式,加载Route时机不同),加载Route需要加载所有Route下的GatewayFilter,RequestRateLimiterGatewayFilterFactory#apply返回一个GatewayFilter。

我们重点看运行时的这个GatewayFilter的逻辑。

SpringCloudGateway源码(四)限流组件

总体上分为两步,第一步KeyResolver解析key,第二步RateLimiter限流判断。

KeyResolver通过本次请求解析出需要限流的资源标识,比如针对uri限流,针对uri+用户限流等。

如果KeyResolver解析key为空,默认会拒绝客户端访问,返回403。

这个行为可以全局设置spring.cloud.gateway.filter.request-rate-limiter.denyEmptyKey=false修改;

也可以通过编码方式或配置文件方式针对单路由修改,比如:

SpringCloudGateway源码(四)限流组件

KeyResolver解析出resourceKey后,代入RateLimiter的isAllowed判断,是否允许请求通过。

如果不允许通过,默认返回429状态码。

3、RedisRateLimiter

RateLimiter是允许用户自定义实现的,只需要实现isAllowed方法,看一下方法定义。

SpringCloudGateway源码(四)限流组件

入参:

routeId=路由id,id=KeyResolver解析的resourceKey。

出参:

allowed=是否允许通过,tokensRemaining=剩余token数量,headers=加入响应头的参数。

这里我们着重分析scg提供的基于Redis的限流器RedisRateLimiter。

RedisRateLimiter核心逻辑都在lua脚本中,我们先搞清楚lua脚本的上下文逻辑。

SpringCloudGateway源码(四)限流组件

每个资源涉及两个key:

1)request_rate_limiter.{resourceKey}.tokens:资源的令牌数量;

2)request_rate_limiter.{resourceKey}.timestamp:一个时间戳,代表上次经过rateLimiter的时间(其实是上次填充令牌桶的时间);

对resourceKey外边加了花括号,是因为如果redisTemplate底层使用官方redis-cluster,需要使用hashtag将两个key路由到同一个slot上。

SpringCloudGateway源码(四)限流组件

lua脚本的args参数有四个:

1)replenishRate:令牌每秒填充速率;

2)burstCapacity:令牌桶最大容量;

3)Instant.now().getEpochSecond():当前时间戳;

4)requestedTokens:每次请求消耗令牌数量,认为是1即可;

SpringCloudGateway源码(四)限流组件

script出参有两个:

1)第一个allowed:1-通过,0-不通过

2)第二个tokensLeft:剩余令牌数量

如果执行lua脚本出错,比如redis挂了,script出参降级为(1,-1),即通过。

接下来重点分析一下lua脚本:META-INF/scripts/request_rate_limiter.lua。

首先计算填满空桶的用时fill_time=桶容量/填充速率=burstCapacity/replenishRate。

此外计算一个生存时间ttl=填桶用时*2向下取整。

SpringCloudGateway源码(四)限流组件

获取last_tokens剩余token数量,默认为桶容量。

SpringCloudGateway源码(四)限流组件

获取last_refreshed上次令牌桶填充时间,默认为0。

SpringCloudGateway源码(四)限流组件

计算计划令牌数量filled_tokens=未填充时间长度delta*填充速率rate+剩余token数量last_tokens,最大不超过桶总容量capacity。

SpringCloudGateway源码(四)限流组件

如果计划令牌数量大于等于1,则allowed=true,allowed_num=1,允许通过,最终令牌数量new_tokens=计划令牌数量-1。

如果计划令牌数量小于1,则allowed=false,allowed_num=0,不允许通过,最终令牌数量new_tokens=计划令牌数量。

SpringCloudGateway源码(四)限流组件

最后,设置两个key的value并设置ttl,返回allow_num是否通过和new_tokens最终令牌数量。

SpringCloudGateway源码(四)限流组件

这里判断ttl>0还有个隐含逻辑,如果用户配置replenishRate:burstCapacity超过2,则这两个key根本不会存入redis,按照lua脚本逻辑每次令牌桶都是满的,请求会被直接放行

由于对lua也不懂,仅仅是凭借变量名和方法名来揣测了这个逻辑,如果要验证这个猜想,可以通过redis-cli的monitor命令监控redis客户端命令。

比如按照21填充速率+10桶容量配置:

SpringCloudGateway源码(四)限流组件

客户端没有发送setex:

SpringCloudGateway源码(四)限流组件

但是按照20填充速率+10桶容量配置,客户端就发送了setex,且ttl=1:

SpringCloudGateway源码(四)限流组件

三、和Sentinel对比

Sentinel可以支持很多流量防护规则,这里仅针对网关流控规则。

Sentinel的网关限流规则适配了热点参数规则,相关文章之前分享过,源码见ParamFlowChecker#passDefaultLocalCheck。

SpringCloudGateway源码(四)限流组件

动态更新

spc默认情况下不支持路由在运行时更新,需要做二次开发。

不过一般情况下都会对RouteLocator和RouteDefinitionLocator做一些二次开发,满足路由动态更新,限流规则顺便也能给一起做了,并不是什么难事。

Sentinel提供Dashboard支持运行时对流控配置做增删改查,开箱即用。

资源key解析

Sentinel支持多种资源key解析方式,开箱即用,比如ip、host等等,但是不支持比如spi扩展。

虽然scg不支持这么多开箱即用的key解析方式,但是可以根据业务定制逻辑,只需要实现KeyResolver即可。

阈值类型/流控效果

Sentinel阈值类型支持QPS和并发线程数,scg自带的RateLimiter仅支持QPS。

Sentinel在阈值类型为QPS的基础上,还支持设置流控效果,默认令牌桶算法快速失败,也支持漏桶算法允许排队。

scg仅支持令牌桶算法。

单机流控or集群流控

Sentinel针对SpringCloudGateway提供了网关流控规则,底层适配了热点参数流控规则,是进程级别的单机流控。Sentinel仅针对普通的流控规则提供了集群模式。

SpringCloudGateway源码(四)限流组件

scg借助Redis实现了集群流控(令牌桶在jvm内存还是在外部集中存储的区别)。

如果KeyResolver做特殊实现,也可以支持单机流控。举个例子,在KeyResolver中加入类变量instanceId作为进程唯一标识。

SpringCloudGateway源码(四)限流组件

但是这样做也不需要用RedisRateLimiter了,自己把lua脚本翻成java代码(比如guava的RateLimiter)实现一个RateLimiter即可。

桶容量/填充速率/时间窗口

桶容量:

Sentinel,桶容量=QPS+burstSize,其中QPS一般用户都会配置,burstSize默认是0。

scg,桶容量=burstCapacity。

填充速率:

Sentinel,填充速率=QPS。

scg,填充速率=replenishRate。

时间窗口:

Sentinel可以自由配置,默认是1秒,前一秒剩余的令牌,不会给后一秒用。

scg,时间窗口取决于ttl,而ttl=math.floor(桶容量/填充速率 * 2),只要key没有过期,桶里剩余的令牌都可以持续使用。

总结

本章分析了SpringCloudGateway自带的限流组件。

scg通过RequestRateLimiterGatewayFilterFactory#apply为每个Route路由创建一个限流过滤器GatewayFilter。

限流过滤器主要包含两个可扩展组件:

1)KeyResolver:解析资源key,比如基于uri限流、基于uri+用户id限流等,提供默认实现PrincipalNameKeyResolver,基于java.security.Principal#getName获取限流资源key;

2)RateLimiter:限流器,根据资源key判断请求是否能通过限流校验,默认提供基于Redis的实现RedisRateLimiter;

与Sentinel相比(仅针对网关流控规则),scg限流组件短板是:

1)无法在运行时动态更新,需要二次开发,但是一般用scg都有定制,这点可以忽略;

2)资源key解析多半需要用户自己实现;

3)仅支持基于QPS限流,且流控效果仅支持令牌桶算法;

优势是:

1)限流器RateLimiter可扩展,借助scg自带的RedisRateLimiter可实现集群流控,也可以二次开发借助三方限流器实现单机流控;

2)资源key解析可通过KeyResolver扩展;

3)可以不用引入sentinel三方组件,而redis比较常用没什么成本;

4)相较于Sentinel,scg的限流器更容易理解,简单明了;

5)RedisRateLimiter+KeyResolver可移植到需要集群限流的地方,比如网关之下的业务应用,具备通用性;

SpringCloudGateway源码(四)限流组件

原文始发于微信公众号(程序猿阿越):SpringCloudGateway源码(四)限流组件

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/214723.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!