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脚本;

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可移植到需要集群限流的地方,比如网关之下的业务应用,具备通用性;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值