Spring Cloud升级之路 - Hoxton - 9. 针对网关非 Get 请求的重试

简介: Spring Cloud升级之路 - Hoxton - 9. 针对网关非 Get 请求的重试

针对网关非 Get 请求的重试


在之前的系列里面Spring Cloud升级之路 - Hoxton - 5. 实现微服务调用重试,我们针对 OpenFeign 和 Spring Cloud Gateway 都设置了重试。

对于 OpenFeign:

  • Get请求:任何非200 响应码,任何异常,都会重试。
  • 非 Get 请求:任何IOException(除了SocketTimeOutException,这个是read time out 导致的),还有 redilience 断路器异常,都会重试,其他的都不重试。

对于 Spring Cloud Gateway:

  • Get请求:任何4XX,5XX响应码,任何异常,都会重试。

现在,我们需要实现针对于 Spring Cloud Gateway 的非 Get 请求的任何IOException(除了SocketTimeOutException,这个是read time out 导致的),还有 redilience 断路器异常进行重试,Get因为请求并没有真正发出去。


现有设计


目前在 Spring Cloud Gateway 的 RetryFilterFactory,无法实现针对 Get 和非 Get 对于不同的异常进行不同的重试:


org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory

public class RetryGatewayFilterFactory
    extends AbstractGatewayFilterFactory<RetryGatewayFilterFactory.RetryConfig> {
  /**
   * Retry iteration key.ServerWebExchange的某个Attribute的key
   * 这个Attribute用来在每次调用的时候,+1,看是否超过了重试次数
   */
  public static final String RETRY_ITERATION_KEY = "retry_iteration";
  public RetryGatewayFilterFactory() {
    super(RetryConfig.class);
  }
  @Override
  public GatewayFilter apply(RetryConfig retryConfig) {
      //检验配置
    retryConfig.validate();
    Repeat<ServerWebExchange> statusCodeRepeat = null;
    //如果配置了可重试的HTTP响应状态码,则检查响应码是否可以重试
    if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) {
      Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> {
        ServerWebExchange exchange = context.applicationContext();
        //检查是否超过了重试次数
        if (exceedsMaxIterations(exchange, retryConfig)) {
          return false;
        }
                //判断是否可以重试
        HttpStatus statusCode = exchange.getResponse().getStatusCode();
        boolean retryableStatusCode = retryConfig.getStatuses()
            .contains(statusCode);
        if (!retryableStatusCode && statusCode != null) { 
          // try the series
          retryableStatusCode = retryConfig.getSeries().stream()
              .anyMatch(series -> statusCode.series().equals(series));
        }
        final boolean finalRetryableStatusCode = retryableStatusCode;
                //判断是否是可以重试的HTTP方法
        HttpMethod httpMethod = exchange.getRequest().getMethod();
        boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
                //返回是否是可以重试的方法以及是否可以重试的HTTP响应状态码
        return retryableMethod && finalRetryableStatusCode;
      };
            //每次重试,都要重置路由,重新解析路由
      statusCodeRepeat = Repeat.onlyIf(repeatPredicate)
          .doOnRepeat(context -> reset(context.applicationContext()));
            //设置Backoff
      BackoffConfig backoff = retryConfig.getBackoff();
      if (backoff != null) {
        statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff));
      }
    }
    // TODO: support timeout, backoff, jitter, etc... in Builder
        //判断异常是否可以重试
    Retry<ServerWebExchange> exceptionRetry = null;
    if (!retryConfig.getExceptions().isEmpty()) {
      Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
        ServerWebExchange exchange = context.applicationContext();
        if (exceedsMaxIterations(exchange, retryConfig)) {
          return false;
        }
        Throwable exception = context.exception();
        for (Class<? extends Throwable> retryableClass : retryConfig
            .getExceptions()) {
          if (retryableClass.isInstance(exception) || (exception != null
              && retryableClass.isInstance(exception.getCause()))) {
            trace("exception or its cause is retryable %s, configured exceptions %s",
                () -> getExceptionNameWithCause(exception),
                retryConfig::getExceptions);
            HttpMethod httpMethod = exchange.getRequest().getMethod();
            boolean retryableMethod = retryConfig.getMethods()
                .contains(httpMethod);
            trace("retryableMethod: %b, httpMethod %s, configured methods %s",
                () -> retryableMethod, () -> httpMethod,
                retryConfig::getMethods);
            return retryableMethod;
          }
        }
        trace("exception or its cause is not retryable %s, configured exceptions %s",
            () -> getExceptionNameWithCause(exception),
            retryConfig::getExceptions);
        return false;
      };
      exceptionRetry = Retry.onlyIf(retryContextPredicate)
          .doOnRetry(context -> reset(context.applicationContext()))
          .retryMax(retryConfig.getRetries());
      BackoffConfig backoff = retryConfig.getBackoff();
      if (backoff != null) {
        exceptionRetry = exceptionRetry.backoff(getBackoff(backoff));
      }
    }
    GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat,
        exceptionRetry);
    return new GatewayFilter() {
      @Override
      public Mono<Void> filter(ServerWebExchange exchange,
          GatewayFilterChain chain) {
        return gatewayFilter.filter(exchange, chain);
      }
      @Override
      public String toString() {
        return filterToStringCreator(RetryGatewayFilterFactory.this)
            .append("retries", retryConfig.getRetries())
            .append("series", retryConfig.getSeries())
            .append("statuses", retryConfig.getStatuses())
            .append("methods", retryConfig.getMethods())
            .append("exceptions", retryConfig.getExceptions()).toString();
      }
    };
  }
  private String getExceptionNameWithCause(Throwable exception) {
    if (exception != null) {
      StringBuilder builder = new StringBuilder(exception.getClass().getName());
      Throwable cause = exception.getCause();
      if (cause != null) {
        builder.append("{cause=").append(cause.getClass().getName()).append("}");
      }
      return builder.toString();
    }
    else {
      return "null";
    }
  }
  private Backoff getBackoff(BackoffConfig backoff) {
    return Backoff.exponential(backoff.firstBackoff, backoff.maxBackoff,
        backoff.factor, backoff.basedOnPreviousValue);
  }
  public boolean exceedsMaxIterations(ServerWebExchange exchange,
      RetryConfig retryConfig) {
    Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY);
    //是否超过了可重试次数
    boolean exceeds = iteration != null && iteration >= retryConfig.getRetries();
    return exceeds;
  }
  public void reset(ServerWebExchange exchange) {
    //这个方法主要是
    Set<String> addedHeaders = exchange.getAttributeOrDefault(
        CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet());
    addedHeaders
        .forEach(header -> exchange.getResponse().getHeaders().remove(header));
    removeAlreadyRouted(exchange);
  }
  public GatewayFilter apply(String routeId, Repeat<ServerWebExchange> repeat,
      Retry<ServerWebExchange> retry) {
    if (routeId != null && getPublisher() != null) {
      // send an event to enable caching
      getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId));
    }
    return (exchange, chain) -> {
      trace("Entering retry-filter");
      // chain.filter returns a Mono<Void>
      Publisher<Void> publisher = chain.filter(exchange)
          // .log("retry-filter", Level.INFO)
          .doOnSuccessOrError((aVoid, throwable) -> {
            int iteration = exchange
                .getAttributeOrDefault(RETRY_ITERATION_KEY, -1);
            int newIteration = iteration + 1;
            trace("setting new iteration in attr %d", () -> newIteration);
            exchange.getAttributes().put(RETRY_ITERATION_KEY, newIteration);
          });
      if (retry != null) {
        // retryWhen returns a Mono<Void>
        // retry needs to go before repeat
        publisher = ((Mono<Void>) publisher)
            .retryWhen(retry.withApplicationContext(exchange));
      }
      if (repeat != null) {
        // repeatWhen returns a Flux<Void>
        // so this needs to be last and the variable a Publisher<Void>
        publisher = ((Mono<Void>) publisher)
            .repeatWhen(repeat.withApplicationContext(exchange));
      }
      return Mono.fromDirect(publisher);
    };
  }
  @SuppressWarnings("unchecked")
  public static class RetryConfig implements HasRouteId {
        //路由id
    private String routeId;
        //重试次数,不包括调用的第一次,默认为3,也就是可能会调用4次
    private int retries = 3;
        //针对哪些HTTP状态码重试,一个Series对应一组HttpStatus
    private List<Series> series = toList(Series.SERVER_ERROR);
        //针对哪些HTTP状态码重试,一个HttpStatus就是一个HTTP状态码
    private List<HttpStatus> statuses = new ArrayList<>();
        //针对哪些HTTP方法重试
    private List<HttpMethod> methods = toList(HttpMethod.GET);
        //针对的哪些异常重试
    private List<Class<? extends Throwable>> exceptions = toList(IOException.class,
        TimeoutException.class);
        //重试间隔策略
    private BackoffConfig backoff;
        public void validate() {
            //重试次数必须大于10
      Assert.isTrue(this.retries > 0, "retries must be greater than 0");
      //可重试的series,可重试的状态码还有可重试的异常不能都为空,否则没有可以重试的场景了
      Assert.isTrue(
          !this.series.isEmpty() || !this.statuses.isEmpty()
              || !this.exceptions.isEmpty(),
          "series, status and exceptions may not all be empty");
      //重试的Http方法不能为空
      Assert.notEmpty(this.methods, "methods may not be empty");
      if (this.backoff != null) {
        this.backoff.validate();
      }
    }
        //省略构造器,getter,setter还有一些工具方法
  }
  public static class BackoffConfig {
      //第一次重试时间间隔
    private Duration firstBackoff = Duration.ofMillis(5);
        //最大等待间隔
    private Duration maxBackoff;
        //增长比例
    private int factor = 2;
        //是否保留上一次请求的重试间隔时间,下次从这个时间间隔开始重试
    private boolean basedOnPreviousValue = true;
    //省略构造器,getter,setter
    public void validate() {
        //第一次重试间隔不能为空
      Assert.notNull(this.firstBackoff, "firstBackoff must be present");
    }
  }
}


总结起来,流程简化如下:

  1. 判断本次请求 HTTP 方法是否被 RetryConfig.methods 包含和 HTTP 响应码是否在 RetryConfig.series 的范围内或者 statuses 的集合内,如果在,看本次请求的 retry_iteration 这个 Attribute 是第几次(从0开始),是否超过了重试次数,如果没超过,就重试,如果超过,停止重试。
  2. 判断本次请求 HTTP 方法是否被 RetryConfig.methods 包含和 异常是否在 RetryConfig.exceptions 的集合内(是其中的某个异常的子类也可以),如果在,看本次请求的 retry_iteration 这个 Attribute 是第几次(从0开始),是否超过了重试次数,如果没超过,就重试,如果超过,停止重试。


配置的时候,HTTP 方法如果包含所有方法,那么没办法区分 GET 请求或者是 非 GET 请求;如果建立两个 Filter 一个拦截 GET 另一个拦截 非GET,那么他们共用的 Attribute 每次就会 +2,重试次数就不准确了。

所以,最后使用了这样一个不优雅的设计,就是 GET 和非 GET 使用不同的 RetryConfig,GET 的还是根据application.properties配置来,针对非 GET 请求,强制重试下面这些异常:

  • io.netty.channel.ConnectTimeoutException.class:连接超时
  • java.net.ConnectException.class:No route to host 异常
  • io.github.resilience4j.circuitbreaker.CallNotPermittedException: resilience4j 断路器相关异常

RetryGatewayFilter

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //获取微服务名称
        String serviceName = request.getHeaders().getFirst(CommonConstant.SERVICE_NAME);
        HttpMethod method = exchange.getRequest().getMethod();
        //生成 GatewayFilter,保存到 gatewayFilterMap
        GatewayFilter gatewayFilter = gatewayFilterMap.computeIfAbsent(serviceName + ":" + method, k -> {
            Map<String, RetryConfig> retryConfigMap = apiGatewayRetryConfig.getRetry();
            //通过微服务名称,获取重试配置
            RetryConfig retryConfig = retryConfigMap.containsKey(serviceName) ? retryConfigMap.get(serviceName) : apiGatewayRetryConfig.getDefault();
            //重试次数为0,则不重试
            if (retryConfig.getRetries() == 0) {
                return null;
            }
            //针对非GET请求,强制限制重试并且只能重试下面的异常b
            if (!HttpMethod.GET.equals(method)) {
                RetryConfig newConfig = new RetryConfig();
                BeanUtils.copyProperties(retryConfig, newConfig);
                //限制所有方法都可以重试,由于外层限制了不为GET,这里相当于不为GET的所有方法
                newConfig.setMethods(HttpMethod.values());
                newConfig.setSeries();
                newConfig.setStatuses();
                newConfig.setExceptions(//链接超时
                        io.netty.channel.ConnectTimeoutException.class,
                        //No route to host
                        java.net.ConnectException.class,
                        //针对Resilience4j的异常
                        CallNotPermittedException.class);
                retryConfig = newConfig;
            }
            return this.apply(retryConfig);
        });
        return gatewayFilter != null ? gatewayFilter.filter(exchange, chain) : chain.filter(exchange);
    }
相关文章
|
25天前
|
消息中间件 监控 Java
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
31 6
|
25天前
|
Java 关系型数据库 MySQL
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
45 5
|
25天前
|
缓存 监控 Java
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
35 5
|
2月前
|
缓存 网络协议 API
【Azure 环境】请求经过应用程序网关,当响应内容大时遇见504超时报错
应用程序网关的响应缓冲区可以收集后端服务器发送的全部或部分响应数据包,然后再将它们发送给客户端。 默认在应用程序网关上启用响应缓冲,这对于适应缓慢的客户端很有用。
|
5月前
|
负载均衡 Java Spring
Spring cloud gateway 如何在路由时进行负载均衡
Spring cloud gateway 如何在路由时进行负载均衡
597 15
|
4月前
|
API 开发者 Java
API 版本控制不再难!Spring 框架带你玩转多样化的版本管理策略,轻松应对升级挑战!
【8月更文挑战第31天】在开发RESTful服务时,为解决向后兼容性问题,常需进行API版本控制。本文以Spring框架为例,探讨四种版本控制策略:URL版本控制、请求头版本控制、查询参数版本控制及媒体类型版本控制,并提供示例代码。此外,还介绍了通过自定义注解与过滤器实现更灵活的版本控制方案,帮助开发者根据项目需求选择最适合的方法,确保API演化的管理和客户端使用的稳定与兼容。
216 0
|
5月前
|
Java Spring
spring cloud gateway在使用 zookeeper 注册中心时,配置https 进行服务转发
spring cloud gateway在使用 zookeeper 注册中心时,配置https 进行服务转发
128 3
|
5月前
|
消息中间件 Java Nacos
通用快照方案问题之通过Spring Cloud实现配置的自动更新如何解决
通用快照方案问题之通过Spring Cloud实现配置的自动更新如何解决
81 0
|
5月前
|
缓存 监控 Java
通用快照方案问题之Spring Boot Admin的定义如何解决
通用快照方案问题之Spring Boot Admin的定义如何解决
72 0