首页 > 其他分享 >Spring Cloud Gateway 网关限流

Spring Cloud Gateway 网关限流

时间:2023-01-18 22:35:40浏览次数:63  
标签:网关 请求 exchange Spring KeyResolver rate 限流 limiter

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

标签:网关,请求,exchange,Spring,KeyResolver,rate,限流,limiter
From: https://www.cnblogs.com/zn-pi/p/17060754.html

相关文章

  • 学习笔记——Spring声明式事务管理;Spring中支持事务管理;使用声明式事务管理;Spring声明
    2023-01-18一、Spring声明式事务管理1、事务四大特征(ACID)(1)原子性(2)一致性(3)隔离性(4)持久性2、事务三种行为(1)开启事务:connection.setAutoCommit(False)(2)提交事务:conne......
  • Spring MVC Tiles示例
    Spring提供了与apachetile框架的集成支持。因此,我们可以借助SpringTile支持简单地管理SpringMVC应用程序的布局。SpringMVC支持Tiles的优势SpringMVCTiles示例1、......
  • Spring MVC自定义验证
    SpringMVC框架允许我们执行自定义验证。在这种情况下,我们声明自己的注释。我们可以根据自己的业务逻辑执行验证。SpringMVC自定义验证示例在此示例中,我们同时使用预......
  • Spring MVC编号验证
    在SpringMVC验证中,我们可以在数字范围内验证用户的输入。以下注释用于实现数字验证:@Min注解-必须传递带有@Min批注的整数值。用户输入必须等于或大于此值。@Max注解......
  • Spring MVC验证
    SpringMVC验证用于限制用户提供的输入。为了验证用户的输入,Spring4或更高版本支持并使用Bean验证API。它可以同时验证服务器端和客户端应用程序。 Bean验......
  • Spring MVC正则表达式验证
    SpringMVC验证使我们可以按特定顺序(即正则表达式)验证用户输入。 @Pattern 批注用于实现正则表达式验证。在这里,我们可以为 regexp 属性提供所需的正则表达式,并将其......
  • day05-Spring管理Bean-IOC-03
    Spring管理Bean-IOC-032.基于XML配置bean2.15bean的生命周期bean对象的创建是由JVM完成的,然后执行如下方法:执行构造器执行set相关方法调用bean的初始化方法(需要配置......
  • Spring MVC文件上传示例
    SpringMVC提供了一种上传文件的简便方法,它可以是图像或其他文件。让我们看一个使用SpringMVC上传文件的简单示例。必需的Jar文件要运行此示例,您需要加载:SpringCore......
  • Spring MVC分页示例
    分页用于在不同部分显示大量记录。在这种情况下,我们将在一页中显示10、20或50条记录。对于其余记录,我们提供链接。我们可以在SpringMVC中简单地创建分页示例。在此分......
  • Spring MVC CRUD示例
    CRUD(创建,读取,更新和删除)应用程序是用于创建任何项目的最重要的应用程序。它提供了开发大型项目的想法。在SpringMVC中,我们可以开发一个简单的CRUD应用程序。在这里,我......