前言
如果不使用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脚本;
GatewayAutoConfiguration默认提供了一个KeyResolver的实现PrincipalNameKeyResolver,基于java.security.Principal#getName获取限流资源key,在案例中我们使用ExampleKeyResolver替换了默认实现。
GatewayAutoConfiguration在RateLimiter和KeyResolver存在的情况下,注入RequestRateLimiterGatewayFilterFactory限流过滤器工厂,用于创建限流过滤器。
2、RequestRateLimiterGatewayFilterFactory
成员变量:
1)defaultRateLimiter:默认全局RateLimiter,如果针对route路由没有定制,默认是RedisRateLimiter;
2)defaultKeyResolver:默认全局KeyResolver,如果针对route路由没有定制,默认是PrincipalNameKeyResolver;
3)denyEmptyKey:是否拒绝KeyResolver解析为空key的请求,默认为true;
4)emptyKeyStatusCode:如果拒绝空key,返回http状态码,默认403Forbidden;
在ioc容器启动阶段(不同路由配置方式,加载Route时机不同),加载Route需要加载所有Route下的GatewayFilter,RequestRateLimiterGatewayFilterFactory#apply返回一个GatewayFilter。
我们重点看运行时的这个GatewayFilter的逻辑。
总体上分为两步,第一步KeyResolver解析key,第二步RateLimiter限流判断。
KeyResolver通过本次请求解析出需要限流的资源标识,比如针对uri限流,针对uri+用户限流等。
如果KeyResolver解析key为空,默认会拒绝客户端访问,返回403。
这个行为可以全局设置spring.cloud.gateway.filter.request-rate-limiter.denyEmptyKey=false修改;
也可以通过编码方式或配置文件方式针对单路由修改,比如:
KeyResolver解析出resourceKey后,代入RateLimiter的isAllowed判断,是否允许请求通过。
如果不允许通过,默认返回429状态码。
3、RedisRateLimiter
RateLimiter是允许用户自定义实现的,只需要实现isAllowed方法,看一下方法定义。
入参:
routeId=路由id,id=KeyResolver解析的resourceKey。
出参:
allowed=是否允许通过,tokensRemaining=剩余token数量,headers=加入响应头的参数。
这里我们着重分析scg提供的基于Redis的限流器RedisRateLimiter。
RedisRateLimiter核心逻辑都在lua脚本中,我们先搞清楚lua脚本的上下文逻辑。
每个资源涉及两个key:
1)request_rate_limiter.{resourceKey}.tokens:资源的令牌数量;
2)request_rate_limiter.{resourceKey}.timestamp:一个时间戳,代表上次经过rateLimiter的时间(其实是上次填充令牌桶的时间);
对resourceKey外边加了花括号,是因为如果redisTemplate底层使用官方redis-cluster,需要使用hashtag将两个key路由到同一个slot上。
lua脚本的args参数有四个:
1)replenishRate:令牌每秒填充速率;
2)burstCapacity:令牌桶最大容量;
3)Instant.now().getEpochSecond():当前时间戳;
4)requestedTokens:每次请求消耗令牌数量,认为是1即可;
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向下取整。
获取last_tokens剩余token数量,默认为桶容量。
获取last_refreshed上次令牌桶填充时间,默认为0。
计算计划令牌数量filled_tokens=未填充时间长度delta*填充速率rate+剩余token数量last_tokens,最大不超过桶总容量capacity。
如果计划令牌数量大于等于1,则allowed=true,allowed_num=1,允许通过,最终令牌数量new_tokens=计划令牌数量-1。
如果计划令牌数量小于1,则allowed=false,allowed_num=0,不允许通过,最终令牌数量new_tokens=计划令牌数量。
最后,设置两个key的value并设置ttl,返回allow_num是否通过和new_tokens最终令牌数量。
这里判断ttl>0还有个隐含逻辑,如果用户配置replenishRate:burstCapacity超过2,则这两个key根本不会存入redis,按照lua脚本逻辑每次令牌桶都是满的,请求会被直接放行。
由于对lua也不懂,仅仅是凭借变量名和方法名来揣测了这个逻辑,如果要验证这个猜想,可以通过redis-cli的monitor命令监控redis客户端命令。
比如按照21填充速率+10桶容量配置:
客户端没有发送setex:
但是按照20填充速率+10桶容量配置,客户端就发送了setex,且ttl=1:
三、和Sentinel对比
Sentinel可以支持很多流量防护规则,这里仅针对网关流控规则。
Sentinel的网关限流规则适配了热点参数规则,相关文章之前分享过,源码见ParamFlowChecker#passDefaultLocalCheck。
动态更新
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仅针对普通的流控规则提供了集群模式。
scg借助Redis实现了集群流控(令牌桶在jvm内存还是在外部集中存储的区别)。
如果KeyResolver做特殊实现,也可以支持单机流控。举个例子,在KeyResolver中加入类变量instanceId作为进程唯一标识。
但是这样做也不需要用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限流,且流控效果仅支持令牌桶算法;
4)由于底层是热点参数规则,sentinel支持针对不同参数,配置不同的限流规则,比如:user_id=1,qps=1;user_id=2,qps=2,其他user_id,qps=10,而scg只能针对user_id统一配置流控阈值;
优势是:
1)限流器RateLimiter可扩展,借助scg自带的RedisRateLimiter可实现集群流控,也可以二次开发借助三方限流器实现单机流控;
2)资源key解析可通过KeyResolver扩展;
3)可以不用引入sentinel三方组件,而redis比较常用没什么成本;
4)相较于Sentinel,scg的限流器更容易理解,简单明了;
5)RedisRateLimiter+KeyResolver可移植到需要集群限流的地方,比如网关之下的业务应用,具备通用性;