常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法.
两种限流的算法确有其独到之处,其他实现比如滑动时间窗或者三色速率标记法,其实是“漏桶”与“令牌桶”的变种。要么将“漏桶”容积换成了单位时间,要么是按规则将请求标记颜色进行处理,底层还是“令牌”的思想。
A. 漏桶算法:水(请求)以任意的速度先进入到漏桶里,桶以固定的速度出水,当水流入桶的速度大于水流出的速度,就会直接溢出(拒绝服务),可以看出漏桶算法能强行限制数据的传输速率。
入桶:以任意速率往桶中放入水滴。
出桶:以固定速率从桶中流出水滴。
B. 令牌桶算法:系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入令牌,如果桶已经满了,令牌就溢出了;如果桶未满,令牌可以累加。当有新的请求时,会拿走一个TOKEN,如果没有TOKEN可以使用了,就会出现阻塞或者拒绝服务。
入桶:以固定速率往桶中放入水滴。
流出:以任意速率从桶中流出水滴。
令牌桶算法:用于保护服务端,主要用来对调用者频率进行限流,目的是不让服务端被压垮。如果服务端本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制=桶大小),那么实际处理速率可以超过配置的限制(桶大小)。
漏桶算法:用于保护调用方,也就是保护所调用的系统。如果调用的第三方系统本身没有保护机制,或者有流量限制的时候,调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这时即使流量突发也必须舍弃,因为消费能力是第三方决定的。
Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory 过滤器工厂,使用 REDIS 和 LUA 脚本实现了令牌桶的方式。限流规则由KeyResolver接口的具体实现类来决定,常用的规则有:IP、URL、参数等等来进行限流。
官方文档
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
在GATEWAY的.YML文件中进行如下配置(主要是REDIS和RequestRateLimiter):
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 系统模块
- id: servicex-system
uri: lb://servicex-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 稳定速率是通过把replenishRate(补充令牌速度)和burstCapacity(令牌桶容量)设置为相同的值来实现的;
# 可通过设置burstCapacity高于replenishRate来允许临时突发流量。在这种情况下,限流器需要在两次突发请求之间留出一段时间(根据replenishRate),因为连续两次突发将导致请求丢失(HTTP 429 - Too Many Requests).
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率, 指的是允许用户每秒执行多少请求,不丢弃任何请求;
redis-rate-limiter.burstCapacity: 5 # 令牌桶总容量, 指的是用户在一秒钟内允许执行的最大请求数,也就是令牌桶可以保存的令牌数, 如果将此值设置为零将阻止所有请求;
redis-rate-limiter.requestedTokens: 1 # 指的是每个请求消耗多少个令牌, 默认是1.
key-resolver: "#{@pathKeyResolver}" # 指的是限流的时候以什么维度来判断,使用SpEL表达式按名称引用BEAN(REDIS中限流相关的KEY和此处的配置相关).
# 文件服务
- id: servicex-file
uri: lb://servicex-file
predicates:
- Path=/file/**
filters:
- StripPrefix=1
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /*/v2/api-docs
- /csrf
1. 多个限流维度, 需要使用@Primary指定;
2. 在配置文件中指定的限流维度的名字来自该文件,key-resolver: "#{@pathKeyResolver}"。
@Configuration
public class KeyResolverConfiguration {
@Primary
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
@Bean
public KeyResolver parameterKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getHeaders().get("username").get(0));
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getHost());
}
}
在REDIS的DB0数据库中会生成两个KEY,它们的TTL都很短,分别为:
request_rate_limiter.{限流维度名字}.tokens
request_rate_limiter.{限流维度名字}.timestamp
问题:我们的代码如何捕获429状态码并进行封装呢?