参考:
API Rate Limiting with Spring Cloud Gateway
Spring Microservices Security Best Practices
示例配置
按照key-resolver解析出的id(字符串key,用于唯一区分用户、IP等等)进行请求限流,
限流算法采用基于redis lua脚本实现的令牌桶算法,具体配置见如下说明:
server:
port: 8088
spring:
application:
name: mx-gateway-opt
cloud:
# 网关配置
gateway:
# 默认过滤器(对所有route均生效)
default-filters:
# 请求限速配置
- name: RequestRateLimiter
args:
# 如果keyResolver返回空key,则拒绝该请求403,默认true表示拒绝,false则表示允许访问
deny-empty-key: false
# 令牌桶算法每秒补充的token数量(每秒的请求数量)
redis-rate-limiter.replenishRate: 10
# 令牌桶算法token最大数量(每秒的最大请求数量)
redis-rate-limiter.burstCapacity: 15
# 单次请求消费的token数量
redis-rate-limiter.requestedTokens: 1
# 自定义的KeyResolver(从请求exchange解析id,用于区分限流的独立单元,如用户ID、remoteAddr、sessionId等)
key-resolver: "#{@begRateLimiterKeyResolver}"
# redis配置
redis:
database: 2
host: localhost
password: mypassw
port: 6379
timeout: 3000
lettuce:
pool:
max-active: 8
max-idle: 8
max-wait: -1
min-idle: 0
keyresovler默认实现
org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver
package org.springframework.cloud.gateway.filter.ratelimit;
import reactor.core.publisher.Mono;
import org.springframework.web.server.ServerWebExchange;
public class PrincipalNameKeyResolver implements KeyResolver {
/**
* {@link PrincipalNameKeyResolver} bean name.
*/
public static final String BEAN_NAME = "principalNameKeyResolver";
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return exchange.getPrincipal().flatMap(p -> Mono.justOrEmpty(p.getName()));
}
}
RequestRateLimiterGatewayFilterFactory
RequestRateLimiterGatewayFilterFactory调用链路
RequestRateLimiterGatewayFilterFactory
-> RedisRateLimiter -> META/scripts/request_rate_limter.lua
request_rate_limiter.lua
lua脚本保证redis单线程执行,解决分布式网关限流问题(避免多个网关同时调用redis导致数据混乱)
--keys参数:
--request_rate_limiter.{id}.tokens
--request_rate_limiter.{id}.timestamp
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
--对应配置:
--redis-rate-limiter.replenishRate: 每秒需要补充的token数量
--redis-rate-limiter.burstCapacity: 令牌桶中token的最大容量
--当前时间戳(秒)
--redis-rate-limiter.requestedTokens: 单次请求消耗的token数量
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
--redis缓存到期时间(2倍且向下取整),
--即超过ttl时间没人访问(则redis缓存tokens_key, timestamp_key失效不存在),
--则默认令牌桶为满的状态(capacity容量)
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
--获取之前(上次访问后)剩余的token数量(默认capacity)
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)
--获取上次访问的时间戳(默认为0)
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)
--当前时间点补充后的token数量(默认capacity,且最多不操作capacity)
--之前剩下的token数量+上次访问后到现在为止需要添加的token数量delta*rate
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
--是否允许访问(补充后的token数量 >= 当前单次请求消耗的token数量)
local allowed = filled_tokens >= requested
--补充后的token数量
local new_tokens = filled_tokens
--是否允许访问
local allowed_num = 0
--若允许访问
if allowed then
--当前请求过后剩余的token数量=补充后的token数量-当前请求消耗的token数量
new_tokens = filled_tokens - requested
--标记允许访问
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
--记录令牌桶tokens_key, timestamp_key
--超过ttl(需要几秒即可填充满令牌桶)后缓存失效,key不存在则默认为满的状态
--例如rate=100, capacity=150, 则ttl=Math.floor((150/100)*2)=3,
--即超过3秒没人访问,则令牌桶会自动被填充满(key失效为空则表示满状态)
if ttl > 0 then
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
end
-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }
lua脚本调用参数debug截图
redis缓存截图
限流相关的响应header
# burst容量(令牌桶1秒时间内最大容量)
X-RateLimit-Burst-Capacity: 1
# 当前1秒内剩余token数据量
X-RateLimit-Remaining: 0
# 每秒补充的token数量
X-RateLimit-Replenish-Rate: 1
# 当前请求需要消费的token数量
X-RateLimit-Requested-Tokens: 1