Spring Gateway常用过滤器(限流、熔断等)

文章目录

限流(RequestRateLimiter)

核心配置

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: normal-http
          uri: http://localhost:8089
          predicates:
            - Path=/api/normal/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                redis-rate-limiter.requestedTokens: 1

RequestRateLimiter有2个重要的参数:

  1. RateLimiter:具体限流逻辑
  2. KeyResolver:key的解析器,就是根据什么限流,例如:ip、服务名、用户命、参数...

默认使用的RateLimiter是RedisRateLimiter,使用的KeyResolver是PrincipalNameKeyResolver

所以,需要添加依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

PrincipalNameKeyResolver获取的是Principal信息,通常我们是没有到,所以需要我们自定义KeyResolver。

因为没有其他的KeyResolver才会注册PrincipalNameKeyResolver,所以我们直接创建一个新的KeyResolver就可以。

自定义KeyResolver

通过ip来做限流:

java 复制代码
@Bean
KeyResolver ipKeyResolver() {
    return exchange -> {
        InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
        if (remoteAddress == null) {
            return Mono.just("unknown");
        }
        InetAddress address = remoteAddress.getAddress();
        return Mono.just(address.getHostAddress());
    };
}

配置rateLimiter和keyResolver

rateLimiter和keyResolver都可以配置:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: requestratelimiter_route
        uri: http://localhost:8089
        filters:
        - name: RequestRateLimiter
          args:
            rate-limiter: "#{@myRateLimiter}"
            key-resolver: "#{@userKeyResolver}"

多个RateLimiter、KeyResolve注意冲突问题。

java 复制代码
@Bean
@ConditionalOnBean({ RateLimiter.class, KeyResolver.class })
@ConditionalOnEnabledFilter
public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter,
    KeyResolver resolver) {
  return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
}

RedisRateLimiter限流实现说明与测试

RedisRateLimiter使用的是令牌桶算法,我们先来看一下RedisRateLimiter的参数:

  1. redis-rate-limiter.replenishRate: 每秒生成令牌数量
  2. redis-rate-limiter.burstCapacity: 令牌桶的大小
  3. redis-rate-limiter.requestedTokens: 每次请求消耗token数量

Spring Gateway的RedisRateLimiter使用的是lua脚本实现,有兴趣可以看看request_rate_limiter.lua脚本。

我们用Apache的ab来请求接口看看:

sh 复制代码
ab -t 180 -c 10 http://127.0.0.1:8000/api/normal/ok

想了解ab的可以看看:Apache Bench(ab)常用命令与结果解读

Redis的信息:

请求结果:

很多时候,我们需要更灵活的限流控制,例如:每天、每周、每月、每年都请求量限制,怎么做呢?

只需要RateLimiter接口,把RequestRateLimiter过滤器的rate-limiter配置为自己实现的RateLimiter即可。

熔断器(CircuitBreaker)

CircuitBreakerGatewayFilterFactory去哪儿了

很多对Spring Gateway熟悉的朋友,肯定会直接找CircuitBreakerGatewayFilterFactory,发现找不到。

通过找GatewayFilterFactory的实现类,找到一个包含CircuitBreaker的SpringCloudCircuitBreakerResilience4JFilterFactory类。

没错,它就是我们的CircuitBreaker。

那为什么,它不一样呢?

我们在核心概念那一篇已经详细介绍了原理,知道GatewayFilterFactory是由RouteDefinitionRouteLocator处理。

java 复制代码
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
    List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories,
    GatewayProperties gatewayProperties, ConfigurationService configurationService) {
  this.routeDefinitionLocator = routeDefinitionLocator;
  this.configurationService = configurationService;
  initFactories(predicates);
  gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
  this.gatewayProperties = gatewayProperties;
}

我们可以看到是通过对name方法获取名字:

java 复制代码
// 存放的是GatewayFilterFactory.name()
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
// RouteDefinitionRouteLocator.loadGatewayFilters
// 使用的时候是通过 FilterDefinition的name查找
GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());

通常都name方法实现使用的是normalizeFilterFactoryName,就是过滤器本身的名字去掉GatewayFilterFactory:

java 复制代码
default String name() {
  return NameUtils.normalizeFilterFactoryName(getClass());
}

public static String normalizeFilterFactoryName(Class<? extends GatewayFilterFactory> clazz) {
  return removeGarbage(clazz.getSimpleName().replace(GatewayFilterFactory.class.getSimpleName(), ""));
}

而SpringCloudCircuitBreakerResilience4JFilterFactory的name方法继承它的父类SpringCloudCircuitBreakerFilterFactory,直接返回来CircuitBreaker。

java 复制代码
public abstract class SpringCloudCircuitBreakerFilterFactory
		extends AbstractGatewayFilterFactory<SpringCloudCircuitBreakerFilterFactory.Config> {

	public static final String NAME = "CircuitBreaker";
  @Override
	public String name() {
		return NAME;
	}
}

为什么要这么做呢?

因为SpringCloudCircuitBreakerResilience4J实在太长了,在配置中写这么长的名字实在不方便。

SpringCloudCircuitBreakerResilience4J原理

其实就是把Filter调用链包装在ReactiveCircuitBreaker中执行。

java 复制代码
@Override
public GatewayFilter apply(Config config) {
  ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId());
  Set<HttpStatus> statuses = config.getStatusCodes().stream().map(HttpStatusHolder::parse)
      .filter(statusHolder -> statusHolder.getHttpStatus() != null).map(HttpStatusHolder::getHttpStatus)
      .collect(Collectors.toSet());

  return new GatewayFilter() {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      return cb.run(chain.filter(exchange).doOnSuccess(v -> {
        // 省略成功处理
      }), t -> {
        // 省略异常处理
        return handle(getDispatcherHandler(), exchange.mutate().request(request).build());
      }).onErrorResume(t -> handleErrorWithoutFallback(t, config.isResumeWithoutError()));
    }
  };
}

我们知道Resilience4j都有不同的实例,不同实例的配置不一样,那么获取的是那一个实例,使用的是什么配置呢?

如果配置了name就使用name,没有name就是要routeId,如果通过他们没有找到就使用默认值。

注意;默认的ReactiveResilience4JCircuitBreaker不仅包含熔断器(CircuitBreaker),还包含超时器(TimeLimiter)

熟悉自动配置的朋友都知道找AutoConfiguration类:

org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration

它会创建:ReactiveResilience4JCircuitBreakerFactory

至于它依赖的注册中心CircuitBreakerRegistry,则要费点功夫找,因为它在resilience4j.springboot包中:

io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.AbstractCircuitBreakerConfigurationOnMissingBean

怎么找到的呢?

开启actuator,引入actuator依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置暴露beans

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: gateway,beans,conditions
  endpoint:
    gateway:
      enabled: true

通过类似http://localhost:8000/actuator/beans链接查看bean来源信息。

其他的就是Resilience4j的东西了,不熟悉Resilience4j的朋友可以看看这篇:resilience4j核心概念、配置参数与基本使用

那么问题来了,我们该怎么自定义配置呢?

自定义熔断配置与使用

不推荐直接创建ReactiveResilience4JCircuitBreakerFactory,我们只需要专注于我们需要的配置就可以。

也不需要直接创建CircuitBreaker,因为SpringCloudCircuitBreakerFilterFactory使用的是org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory#create(java.lang.String, java.lang.String)创建的ReactiveResilience4JCircuitBreaker,它使用的是配置类,而不是实例类。

java 复制代码
@Override
public ReactiveCircuitBreaker create(String id, String groupName) {
  Resilience4JConfigBuilder.Resilience4JCircuitBreakerConfiguration defaultConfig = getConfigurations()
      .computeIfAbsent(id, defaultConfiguration);
  CircuitBreakerConfig circuitBreakerConfig = this.circuitBreakerRegistry.getConfiguration(id)
      .orElseGet(() -> this.circuitBreakerRegistry.getConfiguration(groupName)
          .orElseGet(defaultConfig::getCircuitBreakerConfig));
  TimeLimiterConfig timeLimiterConfig = this.timeLimiterRegistry.getConfiguration(id)
      .orElseGet(() -> this.timeLimiterRegistry.getConfiguration(groupName)
          .orElseGet(defaultConfig::getTimeLimiterConfig));
  Resilience4JConfigBuilder.Resilience4JCircuitBreakerConfiguration config = new Resilience4JConfigBuilder(id)
      .circuitBreakerConfig(circuitBreakerConfig).timeLimiterConfig(timeLimiterConfig).build();
  return new ReactiveResilience4JCircuitBreaker(id, groupName, config, circuitBreakerRegistry,
      timeLimiterRegistry, Optional.ofNullable(circuitBreakerCustomizers.get(id)), isDisableTimeLimiter());
}

我们可以看到它包含熔断配置CircuitBreakerConfig和TimeLimiterConfig,没有就使用默认值。

接下来问题就简单了,我们需要配置熔断器,添加一个熔断器配置类就可以了。

java 复制代码
@Bean
public CircuitBreakerConfig countBasedCircuitBreakerConfig(CircuitBreakerRegistry circuitBreakerRegistry) {
    CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
            .slidingWindowSize(10)
            .minimumNumberOfCalls(20)
            .failureRateThreshold(25f)
            .slowCallRateThreshold(30f)
            .slowCallDurationThreshold(Duration.ofMillis(500))
            .waitDurationInOpenState(Duration.ofSeconds(10))
            .maxWaitDurationInHalfOpenState(Duration.ofSeconds(30))
            .permittedNumberOfCallsInHalfOpenState(10)
            .recordException(throwable ->
                    throwable instanceof RuntimeException &&
                            !(throwable instanceof IllegalArgumentException)
            ).build();
    circuitBreakerRegistry.addConfiguration("custom", config);
    return config;
}

注意我们需要把配置类添加到注册中心CircuitBreakerRegistry,不然找不到,添加到时候都key,我们配置中要使用,这里是custom。

配置文件中,我们可以如下配置:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: circuitbreaker_route
        uri: http://localhost:8089
        filters:
        - CircuitBreaker=custom

CircuitBreaker=custom,前面是熔断器的名字CircuitBreaker

后面是配置的名字custom,这个名字可以随便填,没找到就会使用默认值,但是要使用我们自定义配置,就一定要和我们自定义的配置key对应上。

上面是简写方式,还有非简写方式:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: circuitbreaker_route
        uri: lb://backing-service:8088
        predicates:
        - Path=/consumingServiceEndpoint
        filters:
        - name: CircuitBreaker
          args:
            name: custom
            fallbackUri: forward:/inCaseOfFailureUseThis

我们在Spring Gateway核心概念、流程及原理中介绍了,简写方式是通过构造函数转换,非简写就能直接映射到name和args上。

java 复制代码
public class FilterDefinition {

	@NotNull
	private String name;

	private Map<String, String> args = new LinkedHashMap<>();
}

然后参数map args又会转换为对应的Config类:

java 复制代码
public abstract class SpringCloudCircuitBreakerFilterFactory
		extends AbstractGatewayFilterFactory<SpringCloudCircuitBreakerFilterFactory.Config> {
  public static class Config implements HasRouteId {

      private String name;

      private URI fallbackUri;

      private String routeId;
      public String getId() {
        if (!StringUtils.hasText(name) && StringUtils.hasText(routeId)) {
          return routeId;
        }
        return name;
      }
  }
  ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId());
}

除了配置类,当然可以通过配置文件来配置:

yaml 复制代码
resilience4j:
  circuitbreaker:
    configs:
      xx:
        registerHealthIndicator: true
        failureRateThreshold: 25    # 失败率阈值
        waitDurationInOpenState: 2000 # 断路器半开状态持续时间(ms)
        permittedNumberOfCallsInHalfOpenState: 5 # 半开状态允许的调用次数
        slidingWindowSize: 10
        slidingWindowType: TIME_BASED
    instances:
      backendA:
        baseConfig: xx
      backendB:
        failureRateThreshold: 33

注意:SpringCloudCircuitBreakerFilterFactory使用的不是实例instances,而是configs

所以,对应的是configs下的配置,如果想要覆盖默认配置,只需要在configs下添加一个default配置即可。

路径操作(PrefixPath、StripPrefix、RewritePath、SetPath)

  1. PrefixPath:添加路径前缀
  2. StripPrefix:删除路径前缀层数,例如到网关的请求是/user/query,我们转到user服务,StripPrefix=1就可以把user部分去掉
  3. SetPath:设置路径
  4. RewritePath:路径重新,支持Java表达式,类似Nginx的rewrite
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: rewrite_path_route
          uri: http://localhost:7777
          predicates:
            - Path=/test/**
          filters:
            - RewritePath=/test/?(?<segment>.*), /hello/$\{segment}
        - id: set_path_route
          uri: http://localhost:7777
          predicates:
            - Path=/test/{segment}
          filters:
            - SetPath=/hello/{segment}
        - id: prefix_path_route
          uri: http://localhost:7777
          filters:
            - PrefixPath=/mypath
        - id: strip_prefix_route
          uri: http://localhost:7777
          predicates:
            - Path=/level1/level2/act/**
          filters:
            - StripPrefix=2

在 y a m l 中没有特殊含义, 在yaml中没有特殊含义, 在yaml中没有特殊含义,{segment}在Java正则表达式中是命名分组,但是Spring会也会把它识别为占位符,yaml转义也使用,所以使用${segment}

yaml

RedirectTo

重定向是直接返回来3xx和Locaction,客户端自己做重定向。

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: redirect_path_route
          uri: http://localhost:8089
          predicates:
            - Path=/old/**
          filters:
            - RedirectTo=302, http://127.0.0.1:8088/feign/ok

AddRequestHeade、AddRequestHeadersIfNotPresent 、SetRequestHeader

  1. AddRequestHeader:没有添加,有则跳过
  2. SetRequestHeader:没有添加,有则替换
  3. AddRequestHeadersIfNotPresent:配置多个,逗号分割
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        predicates:
        - Path=/red/{segment}
        filters:
        - AddRequestHeader=X-Request-Red, Blue-{segment}
      - id: setrequestheader_route
        uri: https://example.org
        predicates:
        - Host: {segment}.myhost.org
        filters:
        - SetRequestHeader=foo, bar-{segment}
       - id: add_request_headers_route
        uri: https://example.org
        filters:
        - AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green

都支持从path、host匹配变量

AddResponseHeader、SetResponseHeader、RewriteResponseHeader

  1. AddResponseHeader:没有添加,有则跳过
  2. SetResponseHeader:没有添加,有则替换
  3. RewriteResponseHeader:重写响应头,支持Java正则表达式,更加灵活
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: rewriteresponseheader_route
        uri: https://example.org
        filters:
        - RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***

Retry

重试过滤器,非常实用,一些重要的业务可以添加上。

  1. retries:重试次数,默认3
  2. methods:什么方法会才重试,org.springframework.http.HttpMethod
  3. statuses:什么状态码才需要重试,org.springframework.http.HttpStatus
  4. series:什么状态码系列才重试,默认5xx,org.springframework.http.HttpStatus.Series
  5. exceptions:什么异常才重试,默认IOException、TimeoutException
  6. backoff:重试时间间隔算法,指数退避,就是TCP使用的那个,参数可配置,默认是没有使用这个
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: retry_test
        uri: http://localhost:8080/flakey
        predicates:
        - Host=*.retry.com
        filters:
        - name: Retry
          args:
            retries: 3
            statuses: BAD_GATEWAY
            methods: GET,POST
            backoff:
              firstBackoff: 10ms
              maxBackoff: 50ms
              factor: 2
              basedOnPreviousValue: false

RequestSize

RequestSize用于控制请求大小,类型是DataSize,所以DataUnit都可以使用KB、MB、GB...

不满足条件返回413错误(Payload Too Large)

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: request_size_route
        uri: http://localhost:8080
        predicates:
        - Path=/ai/parse
        filters:
        - name: RequestSize
          args:
            maxSize: 30MB

AddRequestParameter

简单实用,但是不够灵活,因为我们参数往往都是根据服务的动态添加到,它的很大的作用是为我们实现自己添加参数的Filter的参考模板。

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_parameter_route
        uri: https://example.org
        predicates:
        - Host: {segment}.myhost.org
        filters:
        - AddRequestParameter=foo, bar-{segment}
相关推荐
IT 行者14 小时前
Spring Security 6.x CSRF Token增强:从XorCsrfTokenRequestAttributeHandler到安全实践
安全·spring·spring security·csrf
小宇的天下14 小时前
【caibre】快速查看缓存库文件(8)
java·后端·spring
企微自动化14 小时前
企业微信 API 开发:如何实现外部群消息主动推送
java·开发语言·spring
Hello.Reader14 小时前
Flink SQL 性能调优MiniBatch、两阶段聚合、Distinct 拆分、MultiJoin 与 Delta Join 一文打通
sql·spring·flink
while(1){yan}1 天前
Mybatis基础(详解)
spring boot·spring·java-ee·mybatis
一直都在5721 天前
Spring框架:AOP
java·后端·spring
座山雕~1 天前
spring
java·后端·spring
草原印象1 天前
Spring、SpringMVC、Mybatis框架整合实战视频课程
java·spring·mybatis
又是忙碌的一天1 天前
Spring IOC:依赖注入和bean的生命周期
java·后端·spring