SpringCloud 之 Ribbon/Feign/Hystrix 的超时、重试问题总结

4,592 阅读7分钟

Hi,我是空夜,又是一周不见!

今天来讲讲 ribbon 和 feign 中超时时间应该如何配置。

Spring Cloud 中,一般会用 feign 或者 ribbon 来进行服务调用,ribbon 还自带了负载均衡、重试机制。而feign 是基于 ribbon 的。

通常,为了保证服务的高可用,防止雪崩等问题的出现,还会引入 hystrix。hystrix 的熔断也跟超时时间有关系。

如何统筹考虑 ribbon、feign、hystrix 三者之间的关系,添加合适的配置,使得各个组件各司其职、协调合作, 是一个麻烦的问题。

想想就令人头秃。

今天我就来理清一下 ribbon、feign、hystrix 之间的超时关系。

首先有一个推论:

feign 集成了 ribbon 和 hystrix,feign 本身不带超时限制,其超时是由 ribbon 和 hystrix 控制的。

因此,我们仅需要理清 ribbon 和 hystrix 之间的超时关系即可。


下面以 ribbon 为例,分别测试在默认情况下、与 hystrix 整合使用情况下的超时情况。

1. ribbon 的默认配置

ribbon 的默认配置在 DefaultClientConfigImpl 这个类中。

    public static final int DEFAULT_READ_TIMEOUT = 5000;

    public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = 2000;

    public static final int DEFAULT_CONNECT_TIMEOUT = 2000;

注意,这里出现了第一个天坑:虽然 DefaultClientConfigImpl 这个类里指定了 DEFAULT_READ_TIMEOUT 为 5000 ms,但是,debug 发现,这个默认值在构建 ribbon 的 clientConfig 时,被替换掉了。

具体如下:

在使用 ribbon 请求接口时,第一次会构建一个 IClienConfig 对象,这个方法在 RibbonClientConfiguration 类中,此时,重新设置了 ConnectTimeout、ReadTimeout、GZipPayload

public class RibbonClientConfiguration {

    /**
     * Ribbon client default connect timeout.
     */
    public static final int DEFAULT_CONNECT_TIMEOUT = 1000;

    /**
     * Ribbon client default read timeout.
     */
    public static final int DEFAULT_READ_TIMEOUT = 1000;

    /**
     * Ribbon client default Gzip Payload flag.
     */
    public static final boolean DEFAULT_GZIP_PAYLOAD = true;

    @RibbonClientName
    private String name = "client";

    @Autowired
    private PropertiesFactory propertiesFactory;

    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
        config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
        config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
        return config;
    }
    
    //...
}

综上,ribbon 的默认 ConnectTimeout 和 ReadTimeout 都是 1000 ms

下面我们看看自定义配置。


2. 自定义 ribbon 的配置

咱们通过 ribbon.xxx 来自定义配置,看看能不能生效:

ribbon:
  OkToRetryOnAllOperations: true #对所有操作请求都进行重试,默认false
  ReadTimeout: 1000   #负载均衡超时时间,默认值5000
  ConnectTimeout: 3000 #请求连接的超时时间,默认值2000
  MaxAutoRetries: 1    #对当前实例的重试次数,默认0
  MaxAutoRetriesNextServer: 0 #重试切换实例的次数,默认1

测试发现不起作用。怎么肥事?网上有些码友的文章里就是这样写的啊。

原因很简单:必须要添加 ribbon.http.client.enabled = true 的配置,自定义 ribbon 的超时配置才能生效。

ribbon:
  http:
    client:
      enabled: true

下面咱们来测试一下超时和重试机制:

(贴心的我已经给了测试截图了,希望你们爱我,害!)

我这里启用了一个 producer 服务来提供接口,该接口大概是这样的:

    @GetMapping(value = "hello/{name}")
    public String hello(@PathVariable("name") String name, Integer mills) {
        logger.info("开始执行请求,name: " + name + "要求暂停:" + mills + "毫秒");
        if (mills != null && mills > 0) {
            try {
                Thread.sleep(mills);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "hello, [" + name + "], this is service producer by nacos.....";
    }

注意有一个 mills 参数用于指定 producer 接口的等待时间,这样可以测试出 consumer 服务(也就是利用 ribbon 来调用 producer 接口的服务)的超时、重试机制。

consumer 大概是这样的:

    @GetMapping(value = "test")
    //@HystrixCommand(fallbackMethod = "testHystrix")
    public String test(String name, Integer mills) {
        logger.info("开始请求 producer,其暂停时间为:" + mills);
        String producerRes = restTemplate.getForObject(
                "http://" + service_producer_name + "/producer/hello/" + name + "?mills=" + mills, String.class);
        logger.info("请求获取成功,开始打印请求结果:");
        String res = "测试consumer/test接口,基于ribbon调取服务server-producer的hello接口,返回:" + producerRes;
        System.out.println(res);
        return res;
    }

测试代码准备好了,咱们开始测试:

首先,当前实例重试一次:

请求的 ReadTimeOut 设置为 5s,1 + MaxAutoRetries = 2,两次,刚好 10s

我们看下 producer 端:

下面将 MaxAutoRetries 和 MaxAutoRetriesNextServer 都设为 1:

看下 producer 端,每个 5s 接收到一个请求,共 4个:

为什么会请求四次呢?

为什么将 MaxAutoRetriesNextServer 设为 1,会使得请求增加了 2 次重试呢?

MaxAutoRetriesNextServer 直接翻译过来是:下一个服务的最大重试次数。

这个土味翻译听起来就像是对下一个服务重试的次数。那应该是1吗?这里的下一个服务又指的是什么呢?

不要慌,小场面。咱们改一下配置,MaxAutoRetries 设置为 2,MaxAutoRetriesNextServer 设为 3;

这里我们启动两个同名的 producer,那么这个服务就有两个实例了;

测试一下:

(贴心的我已经给在图里给你们标出来分析的结果了,是不是很暖男?)

另一个 producer:

根据日志里的时间推算,MaxAutoRetriesNextServer 真正的意思应该是:如果请求失败,最大要切换多少次服务实例(不管具体有多少个实例,即使一个实例,也会切换回这个实例本身 MaxAutoRetriesNextServer 次 )

下面我们得出结论:ribbon 中,请求最多会被执行—— 1 + maxAutoRetries + (maxAutoRetries + 1) * MaxAutoRetriesNextServer

也就是 (1 + maxAutoRetries ) * (1 + MaxAutoRetriesNextServer) 次

ribbon 这个渣男的套路已经被我们解析透彻了。下面我们将面对 ribbon + hystrix 的渣男+暖男组合。


3. ribbon 集成 hystrix 后的超时、重试配置

为什么要叫 hystrix 暖男呢?当然是有原因的。

hystrix 是一个服务降级、限流、熔断组件。可以有效保证微服务平台的稳定性,避免雪崩等现象的发生。所以说,hystrix 还是很暖的。

ribbon 集成 hystrix 很简单:

启动类添加 @EnableHystrix 注解。

接口上添加 @HystrixCommand ,配置 fallback 方法:

    @GetMapping(value = "test")
    @HystrixCommand(fallbackMethod = "testHystrix")
    public String test(String name, Integer mills) {
        logger.info("开始请求 producer,其暂停时间为:" + mills);
        String producerRes = restTemplate.getForObject(
                "http://" + service_producer_name + "/producer/hello/" + name + "?mills=" + mills, String.class);
        logger.info("请求获取成功,开始打印请求结果:");
        String res = "测试consumer/test接口,基于ribbon调取服务server-producer的hello接口,返回:" + producerRes;
        System.out.println(res);
        return res;
    }

    /**
     * test接口的断路器
     * @param name
     * @return
     */
    private String testHystrix(String name, Integer mills) {
        return "sorry, " + name + ", this service is unavailable temporarily. We are returning the defaultValue by hystrix.";
    }

测试看看:

producer 暂停 500ms, 正常

producer 暂停 1000ms,请求被 @HystrixCommand 指定的 fallback 方法处理:

注意:如果没有配置 fallback,那么 hystrix 的超时就不会生效,而是由 ribbon 来控制。

hystrix 的默认超时时间是 1s,这个配置在 HystrixCommandProperties 类中:

private static final Integer default_executionTimeoutInMilliseconds = 1000; // default => executionTimeoutInMilliseconds: 1000 = 1 second

protected HystrixCommandProperties(HystrixCommandKey key, HystrixCommandProperties.Setter builder, String propertyPrefix) {

    // ...
    this.executionTimeoutEnabled = getProperty(propertyPrefix, key, "execution.timeout.enabled", builder.getExecutionTimeoutEnabled(), default_executionTimeoutEnabled);
    // ...
}

继续测试 ribbon 和 hystrix 超时时间的关系。

配置好 hystrix 的 fallback 后,修改配置文件,设置 hystrix 的超时时间,使其大于 ribbon 的超时时间:

ribbon:
  OkToRetryOnAllOperations: true #对所有操作请求都进行重试,默认false
  ReadTimeout: 2000   #负载均衡超时时间,默认值5000
  ConnectTimeout: 3000 #ribbon请求连接的超时时间,默认值2000
  MaxAutoRetries: 0     #对当前实例的重试次数,默认0
  MaxAutoRetriesNextServer: 0 #对切换实例的重试次数,默认1
  # 如果不添加 ribbon.http.client.enabled=true,那么 ribbon 的默认配置不会生效
  http:
    client:
      enabled: true


hystrix:
  command:
    default:  #default全局有效,service id指定应用有效
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制,为true,则超时作为熔断根据
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 10000 #断路器超时时间,默认1000ms

此时,用 postman 发送消息:

我们可以看到,请求 2s 左右就返回了,这个值刚好是 ribbon.ReadTimeout 的时间。表示此时 ribbon 超时触发了。然后进入了 hystrix 的熔断过程。


4. 结论

总结一下:

  1. 如果请求时间超过 ribbon 的超时配置,会触发重试;

  2. 在配置 fallback 的情况下,如果请求的时间(包括 ribbon 的重试时间),超出了 ribbon 的超时限制,或者 hystrix 的超时限制,那么就会熔断;

一般来说,会设置 ribbon 的超时时间 < hystrix, 这是因为 ribbon 有重试机制。(这里说的 ribbon 超时时间是包括重试在内的,即,最好要让 ribbon 的重试全部执行,直到 ribbon 超时被触发)。

由于 connectionTime 一般比较短,可以忽略。那么,设置的超时时间应该满足:

(1 + MaxAutoRetries) * (1 + MaxAutoRetriesNextServer) ReadTimeOut < hystrix 的 timeoutInMilliseconds*


今日的分享到此结束,记得点个关注点个!我的公众号是:猿生物语(ID:JavaApes