Skip to content

Feign Retry 重试

🏷️ Spring Cloud Feign

为 Spring Cloud Ribbon 配置请求重试(Camden.SR2+) 里说是通过 spring.cloud.loadbalancer.retry.enabled 参数来开启重试机制,但是经过测试发现是通过 ribbon.OkToRetryOnAllOperations 设置为 true 来开启重试,该属性默认值为 false

  • ribbon.ConnectTimeout:请求连接的超时时间
  • ribbon.ReadTimeout:请求处理的超时时间
  • ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试(默认值为 false
  • ribbon.MaxAutoRetriesNextServer:切换实例的重试次数(默认值为 1)
  • ribbon.MaxAutoRetries:对当前实例的重试次数(默认值为 0)

调用次数

调用次数 = (ribbon.MaxAutoRetriesNextServer + 1) * (ribbon.MaxAutoRetries + 1)

ribbon.MaxAutoRetriesNextServerribbon.MaxAutoRetries调用次数
001
102
114
126
203
216
229

配置文件 application.yml

可以在配置文件中使用如下方式配置全局的重试次数或者某个服务的重试次数,也可以单独将某个服务禁用重试机制。

yaml
# Ribbon
ribbon:
  ReadTimeout: 2000 # 请求处理的超时时间
  ConnectTimeout: 10000 # 请求连接的超时时间
  #MaxAutoRetries: 0
  #OkToRetryOnAllOperations: false #Whether all operations can be retried for this client
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 0
  MaxAutoRetriesNextServer: 1

SERVICE-TEST:
  ribbon:
    OkToRetryOnAllOperations: true
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1

SERVICE-NO-RETRYING:
  ribbon:
    OkToRetryOnAllOperations: false

FeignClientsConfiguration

也可以通过代码来实现上述效果。

java
package com.octopus.middle.api.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created by liujiajia on 2019/1/22.
 */
@Configuration
public class FeignRetryConfig {
    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }
}

下面是 Retryer.Default 的源码,可以看出默认是最多重试 5 次(包含首次调用),重试的间隔时间是动态变化的(参照源码 nextMaxInterval 方法),越往后间隔时间越长,但最长不会超过设置的最大间隔(maxPeriod)。

可以 Retryer.Default 的一个重载来指定间隔时间和最大重试次数。

点击查看 Retryer.Default 源码
java
/*
 * Copyright 2013 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package feign;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Cloned for each invocation to {@link Client#execute(Request, feign.Request.Options)}.
 * Implementations may keep state to determine if retry operations should continue or not.
 */
public interface Retryer extends Cloneable {

  /**
   * if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception.
   */
  void continueOrPropagate(RetryableException e);

  Retryer clone();

  public static class Default implements Retryer {

    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;

    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;
      this.maxPeriod = maxPeriod;
      this.maxAttempts = maxAttempts;
      this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }

    public void continueOrPropagate(RetryableException e) {
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
      }
      sleptForMillis += interval;
    }

    /**
     * Calculates the time interval to a retry attempt. <br> The interval increases exponentially
     * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the backoff factor), to the
     * maximum interval.
     *
     * @return time in nanoseconds from now until the next attempt.
     */
    long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
  }

  /**
   * Implementation that never retries request. It propagates the RetryableException.
   */
  Retryer NEVER_RETRY = new Retryer() {

    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }

    @Override
    public Retryer clone() {
      return this;
    }
  };
}

通过代码禁用重试:

java
package com.octopus.middle.api.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created by liujiajia on 2019/1/22.
 */
@Configuration
public class FeignNoRetryingConfig {
    @Bean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }
}

@FeignClient.configuration

上面的配置是作用全局或者某个服务的全局的,如果需要某些接口可以重试,某些不允许重试,则需要使用 @FeignClient 注解的 configuration 参数来实现。

下面是全局禁用重试,仅 TestServiceInterface 允许重试的示例。

FeignNoRetryingConfig.java

java
package com.octopus.middle.api.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created by liujiajia on 2019/1/22.
 */
@Configuration
public class FeignNoRetryingConfig {
    @Bean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }
}

FeignRetryableConfig.java

java
package com.octopus.middle.api.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;

/**
 * Created by liujiajia on 2019/1/22.
 */
public class FeignRetryableConfig {
    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }
}

TestServiceInterface.java

java
package com.octopus.middle.api.services.test;

/**
 * Created by liujiajia on 2019/1/21.
 */
import com.octopus.middle.api.config.FeignRetryableConfig;
import com.octopus.middle.api.model.base.Parameter;
import com.octopus.middle.api.result.base.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Test Service
 */
@FeignClient(name = "${service.test}", configuration = FeignRetryableConfig.class ,fallback = TestService.class)
public interface TestServiceInterface {
    /**
     * Not Found
     *
     * @param parameter
     * @return
     */
    @RequestMapping(value = "api/test/not-found", method = RequestMethod.POST)
    Result notFound(Parameter parameter);

    /**
     * Time Out
     *
     * @param parameter
     * @return
     */
    @RequestMapping(value = "api/test/time-out", method = RequestMethod.POST)
    Result timeOut(Parameter parameter);
}

配置文件和代码同时使用

单独使用代码方式来设置重试时,重试次数貌似是在实例之间平均分配的,比如默认 5 次时,一台调用了 2 次,一台调用了 3 次。
但若是代码和配置文件都做了设置,调用的次数不确定是如何计算的。
比如 ribbon.MaxAutoRetries 设置为 2,代码使用 Retryer.Default() 时,本以为代码的重试次数会覆盖配置文件的设置,但结果调用很多次(日志太乱,不知道数的对不对,总共调用了约 30 次)。

2019/01/23 追记

配置文件和代码同时使用时,经过几次测试,调用次数应该等于 单独使用配置文件时的调用次数 再乘以 代码中设置的重试次数。

调用次数 = (ribbon.MaxAutoRetriesNextServer + 1) * (ribbon.MaxAutoRetries + 1) * Retryer.Default.maxAttempts

使用两个实例(假设为 A 和 B)时的测试结果:

ribbon.MaxAutoRetriesNextServerribbon.MaxAutoRetriesRetryer.Default.maxAttempts调用次数(A)调用次数(B)
00523
10555
1151010
1251812
20596
2151218
2252421

调用次数是符合上面的公式的,但是具体每个服务的实例调用的次数就不确定是如何分配的了。

ribbon 设置都为 0 的时候,应该仅在某台服务实例上重试,但结果是分别调用 2 次和 3 次,没有严格遵守 ribbon.MaxAutoRetriesNextServer 为 0 的设定。

参考

  1. 为 Spring Cloud Ribbon 配置请求重试(Camden.SR2+)