可用性和可靠性对于所有 web 应用程序和 API 来说都是至关重要的。当系统流量突然增加时,会影响应用程序的服务质量,甚至可能导致所有用户的服务中断。一种解决方案是为基础设施增加更多容量以适应用户增长,然而这不能确保不良行为者不会意外或故意影响其可用性。另一种方案是对请求进行限流,它可以使你的 API 更加可靠。限流用于控制网络上发送或接收的流量的速率。
在 Spring Cloud Gateway 中,我们可以使用 RequestRateLimiter
GatewayFilter
Factory 来实现请求限流。
RequestRateLimiter
GatewayFilter
工厂使用一个 RateLimiter
实现来确定当前请求是否可以放行。如果否,则返回 HTTP 429 - Too Many Requests
(默认)状态。
该过滤器接受一个可选的 keyResolver
参数和特定于限流的参数。
keyResolver
是一个实现了 KeyResolver
接口的 bean。在配置中,使用 SpEL 按名称引用 bean。#{@myKeyResolver}
是一个引用名为 myKeyResolver
的 bean 的 SpEL 表达式。KeyResolver
接口:
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
KeyResolver
的默认实现是 PrincipalNameKeyResolver
,它从 ServerWebExchange
中检索 Principal
并调用 Principal.getname()
。
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()));
}
}
默认情况下,如果 KeyResolver
没有找到 key,请求将被拒绝。你可以通过设置 spring.cloud.gateway.filter.request-rate-limititer.deny-empty-key
(true 或 false)和 spring.cloud.gateway.filter.request-rate- limititer.empty-key-status-code
属性来调整这种行为。
注意:RequestRateLimiter
不能用“快捷”方式进行配置。下面的例子是无效的:
# INVALID SHORTCUT CONFIGURATION
spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}
Redis RateLimiter
Redis 的限流实现基于 Stripe。需要使用 spring-boot-starter-data-redis-reactive
启动器。
使用的算法是令牌桶算法。
redis-rate-limiter.replenishRate
属性定义每秒允许多少请求。这是令牌桶被填充的速率。
redis-rate-limiter.burstCapacity
属性是用户在一秒钟内允许的最大请求数。这是令牌桶可以容纳的令牌数量。将该值设置为 0 将阻塞所有请求。
redis-rate-limiter.requestedTokens
属性表示一个请求花费多少令牌。即每个请求从桶中取出的令牌数量,默认为 1。
一个稳定的速率是通过将 replenishRate
和 burstCapacity
设置相同的值来实现的。可以通过将 burstCapacity
设置为高于 replenishRate
来允许临时突发请求。
低于 1个请求/秒 的速率限制可以通过将 replenishRate
设置为所需的请求数量,将 requestedTokens
设置为以秒为单位的时间间隔,将 burstCapacity
设置为 replenishRate
和 requestedTokens
的乘积来实现。例如,设置 replenishRate=1
, requestedTokens=60
, burstCapacity=60
会导致限制为 1 个请求/分。
配置 redis-rate-limiter
的示例如下:
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 定义每秒允许 1 个请求。
redis-rate-limiter.burstCapacity: 3 # 一秒钟内允许的最大 3 个请求。
redis-rate-limiter.requestedTokens: 1 # 一个请求花费 1 个令牌。
在 Java 中配置 KeyResolver
的示例如下:
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
这将每个用户的请求速率限制为 1。允许突发 3 个请求,但是在下一秒,只有 1 个请求可用。KeyResolver
是一个简单的获取用户请求参数的工具。注意:不推荐用于生产环境。
您还可以将速率限制器定义为实现 RateLimiter
接口的 bean。在配置中,您可以使用 SpEL 按名称引用 bean。#{@myRateLimiter}
是一个 SpEL 表达式,它引用一个名为 myRateLimiter
的 bean。
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@myRateLimiter}"
key-resolver: "#{@userKeyResolver}"
在 1s 内发送 5 次请求,只有 3 个请求被允许,剩余两个请求返回 HTTP 429 - Too Many Requests
。
自定义限流状态
默认情况下,限流返回状态为 HTTP 429 - Too Many Requests
。可通过继承 RequestRateLimiterGatewayFilterFactory
并重写 apply
方法来自定义限流响应结果。
@Slf4j
@Component
public class PiRequestRateLimiterGatewayFilterFactory
extends RequestRateLimiterGatewayFilterFactory {
private static final String EMPTY_KEY = "____EMPTY_KEY__";
private final ObjectMapper objectMapper;
@Autowired
public PiRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
KeyResolver defaultKeyResolver,
ObjectMapper objectMapper) {
super(defaultRateLimiter, defaultKeyResolver);
this.objectMapper = objectMapper;
}
@Override
public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {
KeyResolver resolver = getOrDefault(config.getKeyResolver(), super.getDefaultKeyResolver());
@SuppressWarnings("unchecked")
RateLimiter<Object> limiter = getOrDefault(config.getRateLimiter(), super.getDefaultRateLimiter());
boolean denyEmpty = getOrDefault(config.getDenyEmptyKey(), super.isDenyEmptyKey());
HttpStatusHolder emptyKeyStatus = HttpStatusHolder
.parse(getOrDefault(config.getEmptyKeyStatus(), super.getEmptyKeyStatusCode()));
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
if (EMPTY_KEY.equals(key)) {
if (denyEmpty) {
setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
assert route != null;
routeId = route.getId();
}
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
}
ServerHttpResponse serverHttpResponse = exchange.getResponse();
boolean rst = serverHttpResponse.setStatusCode(config.getStatusCode());
if (!rst && log.isWarnEnabled()) {
log.warn("Unable to set status code to " + rst + ". Response already committed.");
}
serverHttpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return serverHttpResponse.writeWith(Mono.create(monoSink -> {
try {
byte[] bytes = objectMapper.writeValueAsBytes(
ResponseData.error(ResponseStatusEnum.REQUEST_RATE_LIMIT));
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(bytes);
monoSink.success(dataBuffer);
} catch (JsonProcessingException e) {
log.error(e.getMessage());
monoSink.error(e);
}
}));
});
});
}
private <T> T getOrDefault(T configValue, T defaultValue) {
return (configValue != null) ? configValue : defaultValue;
}
}
修改过滤器名称为 PiRequestRateLimiter:
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: PiRequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
redis-rate-limiter.requestedTokens: 1