Spring Cloud Gateway 网关限流

可用性可靠性对于所有 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。

一个稳定的速率是通过将 replenishRateburstCapacity 设置相同的值来实现的。可以通过将 burstCapacity 设置为高于 replenishRate 来允许临时突发请求。

低于 1个请求/秒 的速率限制可以通过将 replenishRate 设置为所需的请求数量,将 requestedTokens 设置为以秒为单位的时间间隔,将 burstCapacity 设置为 replenishRaterequestedTokens 的乘积来实现。例如,设置 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

image

自定义限流状态

默认情况下,限流返回状态为 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

image

实战

一个适合学习的 Spring Cloud 开源项目,欢迎 Fork:

Gitee GitHub
后端 https://gitee.com/linjiabin100/pi-cloud.git https://github.com/zengpi/pi-cloud.git
前端 https://gitee.com/linjiabin100/pi-cloud-web.git https://github.com/zengpi/pi-cloud-web.git
posted @ 2023-01-18 22:16  ZnPi  阅读(1252)  评论(0编辑  收藏  举报