Spring Gateway 网关 限流
Spring Gateway 网关 限流
Spring Gateway 提供了很多开箱即用的 Filter
, 今天我们来介绍一下他的限流 RequestRateLimiter , 由于网关是所有请求的入口所以网关统一做限流是最合适的。
首先什么是限流? 为什么需要限流? 限流有两种一种是限制总的QPS, 比如10000 QPS 每秒处理10000个请求,多的就拒绝防止系统过载。 还有一种是结合业务关键字的限流, 想象一下你开了个餐馆,一天能做100个包子,你希望更多的客人品尝到你的包子,可是经常有客人一次购买50个包子,其他排队的客人就没有机会了,于是你限制每人最多购买5个包子。 第一种限流只是限制总的请求量,防止系统过载崩溃,但是想象一下,有人用脚本攻击你的系统,发出大量的请求,流量被占满触发限流,这时候其他正常用户的请求会被拒绝,你的服务虽然没有宕机,但是也无法提供服务,服务不可用和宕机没睡吗区别。 第二种他回根据一些用户标示,位每个不同的用户设置限流,比人一个用户 30个请求/秒,这样防止系统被个别用户请求占满而导致其他正常用户不可用。 spring gateway RequestRateLimiter 就可以实现对用户维度的限流。 使用 RequestRateLimiter 需要我们实现一个接口 :
public interface KeyResolver { Mono<String> resolve(ServerWebExchange exchange); }
这个接口很简单就是需要我们返回一个key, 以这个key 作为维度限流, 以前的文章讲过spring gateway 统一制作 jwt 用户身份鉴权,每次请求头都会携带 Authorization
token,每个用户登录后都会签发一个token,可以用token 作为一个用户的唯一标示
实现如下:
@Component public class TokenKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { String authorization = exchange.getRequest().getHeaders().getFirst(AUTHHEADER); if(org.apache.commons.lang.StringUtils.isNotBlank(authorization)){ return Mono.just(authorization); } return Mono.empty(); }
spring gateway 的限流需要用到 redis,具体实现原理可以参考 https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d ,
使用的request_rate_limiter.lua
脚本实现的令牌桶算法:
local tokens_key = KEYS[1] local timestamp_key = KEYS[2] 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) local last_tokens = tonumber(redis.call("get", tokens_key)) if last_tokens == nil then last_tokens = capacity end local last_refreshed = tonumber(redis.call("get", timestamp_key)) if last_refreshed == nil then last_refreshed = 0 end 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 if allowed then new_tokens = filled_tokens - requested end redis.call("setex", tokens_key, ttl, new_tokens) redis.call("setex", timestamp_key, ttl, now) return { allowed, new_tokens }
我们需要添加依赖来连接redis
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
后面就只需要添加配置就可以了
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: ip: gateway: redis-rate-limiter: includeHeaders: false filter: request-rate-limiter: deny-empty-key: false # empty-key-status-code: 200 default-filters: - name: RequestRateLimiter args: # 令牌桶每秒填充平均速率 redis-rate-limiter.replenishRate: 15 # 令牌桶的上限 redis-rate-limiter.burstCapacity: 30 redis-rate-limiter.requestedTokens: 1 # 使用SpEL表达式从Spring容器中获取Bean对象 key-resolver: "#{@tokenKeyResolver}"
启动测试 可以用wrk 来测试,超过请求限制,就会返回 HTTP 429 - Too Many Requests