本文主要研究一下spring cloud gateway的RetryGatewayFilter

GatewayAutoConfiguration

spring-cloud-gateway-core-2.0.0.RC2-sources.jar!/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

@Configuration
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {
    //......
    @Bean
    public RetryGatewayFilterFactory retryGatewayFilterFactory() {
        return new RetryGatewayFilterFactory();
    }
    //......
}
默认启用了RetryGatewayFilterFactory

RetryGatewayFilterFactory

spring-cloud-gateway-core-2.0.0.RC2-sources.jar!/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java

public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<RetryGatewayFilterFactory.RetryConfig> {
    private static final Log log = LogFactory.getLog(RetryGatewayFilterFactory.class);

    public RetryGatewayFilterFactory() {
        super(RetryConfig.class);
    }

    @Override
    public GatewayFilter apply(RetryConfig retryConfig) {
        retryConfig.validate();

        Predicate<? super RepeatContext<ServerWebExchange>> predicate = context -> {
            ServerWebExchange exchange = context.applicationContext();
            if (exceedsMaxIterations(exchange, retryConfig)) {
                return false;
            }

            HttpStatus statusCode = exchange.getResponse().getStatusCode();
            HttpMethod httpMethod = exchange.getRequest().getMethod();

            boolean retryableStatusCode = retryConfig.getStatuses().contains(statusCode);

            if (!retryableStatusCode && statusCode != null) { // null status code might mean a network exception?
                // try the series
                retryableStatusCode = retryConfig.getSeries().stream()
                        .anyMatch(series -> statusCode.series().equals(series));
            }

            boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
            return retryableMethod && retryableStatusCode;
        };

        Repeat<ServerWebExchange> repeat = Repeat.onlyIf(predicate)
                .doOnRepeat(context -> reset(context.applicationContext()));

        //TODO: support timeout, backoff, jitter, etc... in Builder

        Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
            if (exceedsMaxIterations(context.applicationContext(), retryConfig)) {
                return false;
            }

            for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) {
                if (clazz.isInstance(context.exception())) {
                    return true;
                }
            }
            return false;
        };

        Retry<ServerWebExchange> reactorRetry = Retry.onlyIf(retryContextPredicate)
                .doOnRetry(context -> reset(context.applicationContext()))
                .retryMax(retryConfig.getRetries());
        return apply(repeat, reactorRetry);
    }

    public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
        Integer iteration = exchange.getAttribute("retry_iteration");

        //TODO: deal with null iteration
        return iteration != null && iteration >= retryConfig.getRetries();
    }

    public void reset(ServerWebExchange exchange) {
        //TODO: what else to do to reset SWE?
        exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR);
    }

    @Deprecated
    public GatewayFilter apply(Repeat<ServerWebExchange> repeat) {
        return apply(repeat, Retry.onlyIf(ctxt -> false));
    }

    public GatewayFilter apply(Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
        return (exchange, chain) -> {
            log.trace("Entering retry-filter");

            int iteration = exchange.getAttributeOrDefault("retry_iteration", -1);
            exchange.getAttributes().put("retry_iteration", iteration + 1);

            return Mono.fromDirect(chain.filter(exchange)
                    .log("retry-filter", Level.INFO)
                    .retryWhen(retry.withApplicationContext(exchange))
                    .repeatWhen(repeat.withApplicationContext(exchange)));
        };
    }
    //......
}
  • 可以看到这个filter使用了reactor的Retry组件,同时往exchange的attribues添加retry_iteration,用来记录重试次数,该值默认从-1开始,第一次执行的时候,retry_iteration+1为0。之后每重试一次,就添加1。
  • filter的apply接收两个参数,一个是Repeat<ServerWebExchange>,一个是Retry<ServerWebExchange>。
  • repeat与retry的区别是repeat是在onCompleted的时候会重试,而retry是在onError的时候会重试。这里由于不一定是异常的时候才可能重试,所以加了repeat。

RetryConfig

spring-cloud-gateway-core-2.0.0.RC2-sources.jar!/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java

    public static class RetryConfig {
        private int retries = 3;
        
        private List<Series> series = toList(Series.SERVER_ERROR);
        
        private List<HttpStatus> statuses = new ArrayList<>();
        
        private List<HttpMethod> methods = toList(HttpMethod.GET);

        private List<Class<? extends Throwable>> exceptions = toList(IOException.class);

        //......

        public void validate() {
            Assert.isTrue(this.retries > 0, "retries must be greater than 0");
            Assert.isTrue(!this.series.isEmpty() || !this.statuses.isEmpty(),
                    "series and status may not both be empty");
            Assert.notEmpty(this.methods, "methods may not be empty");
        }
        //......

    }
可以看到配置文件有5个属性,如下:
  • retries,默认为3,用来标识重试次数
  • series,用来指定哪些段的状态码需要重试,默认SERVER_ERROR即5xx
  • statuses,用于指定哪些状态需要重试,默认为空,它跟series至少得指定一个
  • methods,用于指定那些方法的请求需要重试,默认为GET
  • exceptions,用于指定哪些异常需要重试,默认为java.io.IOException

实例

spring:
  cloud:
    gateway:
      routes:
      - id: retry-demo
        uri: http://localhost:9090
        predicates:
        - Path=/retry/**
        filters:
        - name: Retry
          args:
           retries: 15
           series:
            - SERVER_ERROR
            - CLIENT_ERROR
           methods:
            - GET
            - POST
           exceptions:
            - java.io.IOException
            - java.util.concurrent.TimeoutException
这里指定了针对4xx及5xx的GET或POST方法或者IOException或TimeoutException的时候进行重试,重试次数为15次。

小结

RetryGatewayFilter借助了reactor-addons的retry组件进行了重试,主要使用了Mono的repeatWhen及retryWhen方法,前者在onCompleted的时候触发,后者在onError的时候触发。

doc


codecraft
11.9k 声望2k 粉丝

当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下...