美文网首页Spring cloudSpring Cloud 零星知识点
Spring Cloud组件那么多超时设置,如何理解和运用?

Spring Cloud组件那么多超时设置,如何理解和运用?

作者: 亦山札记 | 来源:发表于2019-05-31 22:43 被阅读209次

    前言

    Spring Cloud 作为微服务解决方案 全家桶,集合了丰富的微服务组件,如GatewayFeignHystrix,RibbonOkHttpEureka等等。而作为服务调用环节涉及到的几个组件:FeignHystrix,RibbonOkHttp 都有超时时间的设置,Spring Cloud 是如何优雅地把它们协调好呢?本文将为你揭晓答案。

    1. Spring Cloud 中发起一个接口调用,经过了哪些环节?

    Spring Cloud 在接口调用上,大致会经过如下几个组件配合:
    Feign -----> Hystrix --->Ribbon ---> Http Client(apache http components 或者 Okhttp)
    具体交互流程上,如下图所示:

    Spring Cloud服务调用轨迹
    • 接口化请求调用
      当调用被@FeignClient注解修饰的接口时,在框架内部,会将请求转换成Feign的请求实例feign.Request,然后交由Feign框架处理。
    • Feign :转化请求
      至于Feign的详细设计和实现原理,在此不做详细说明。
      请参考我的另外一篇文章:Spring Cloud Feign 设计原理
    • Hystrix :熔断处理机制
      Feign的调用关系,会被Hystrix代理拦截,对每一个Feign调用请求,Hystrix都会将其包装成HystrixCommand,参与Hystrix的流控和熔断规则。如果请求判断需要熔断,则Hystrix直接熔断,抛出异常或者使用FallbackFactory返回熔断Fallback结果;如果通过,则将调用请求传递给Ribbon组件。
      关于Hystrix的工作原理,参考Spring Cloud Hystrix设计原理
    • Ribbon :服务地址选择
      当请求传递到Ribbon之后,Ribbon会根据自身维护的服务列表,根据服务的服务质量,如平均响应时间,Load等,结合特定的规则,从列表中挑选合适的服务实例,选择好机器之后,然后将机器实例的信息请求传递给Http Client客户端,HttpClient客户端来执行真正的Http接口调用;
      关于Ribobn的工作原理,参考Spring Cloud Ribbon设计原理
    • HttpClient :Http客户端,真正执行Http调用
      根据上层Ribbon传递过来的请求,已经指定了服务地址,则HttpClient开始执行真正的Http请求。
      关于HttpClient的其中一个实现OkHttp的工作原理,请参考Spring Cloud OkHttp设计原理

    2.每个组件阶段的超时设置

    如上一章节展示的调用关系,每个组件自己有独立的接口调用超时设置参数,下面将按照从上到下的顺序梳理:

    2.1 feign的默认配置

    feign 的配置可以采用feign.client.config.<feginName>....的格式为每个feign客户端配置,对于默认值,可以使用feign.client.config.default..的方式进行配置,该配置项在Spring Cloud中,使用FeignClientProperties类表示。

    feign:
      client:
        config:
          <feignName>:
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: full
            errorDecoder: com.example.SimpleErrorDecoder
            retryer: com.example.SimpleRetryer
            requestInterceptors:
              - com.example.FooRequestInterceptor
              - com.example.BarRequestInterceptor
            decode404: false
            encoder: com.example.SimpleEncoder
            decoder: com.example.SimpleDecoder
            contract: com.example.SimpleContract
    

    其中,关于feign的管理连接超时的配置项:

    ## 网络连接时间
    feign.client.config.<clientname>.connectTimeout=
    ## 读超时时间
    feign.client.config.<clientname>.readTimeout=
    

    2.2 Spring Cloud 加载feign配置项的原理:

    1. 检查是否Feign是否制定了上述的配置项,即是否有FeignClientProperties实例;
    2. 如果有上述的配置项,则表明Feign是通过properties初始化的,即configureUsingProperties;
    3. 根据配置项feign.client.defaultToProperties的结果,使用不同的配置覆盖策略。

    feign初始化的过程,其实就是构造Feign.Builder的过程,如下图所示:

    2019-05-29_134522.png
    相关代码实现如下:
    protected void configureFeign(FeignContext context, Feign.Builder builder) {
            FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
            if (properties != null) {
                if (properties.isDefaultToProperties()) {
                    configureUsingConfiguration(context, builder);
                    configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
                    configureUsingProperties(properties.getConfig().get(this.name), builder);
                } else {
                    configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
                    configureUsingProperties(properties.getConfig().get(this.name), builder);
                    configureUsingConfiguration(context, builder);
                }
            } else {
                configureUsingConfiguration(context, builder);
            }
        }
    

    2.3.场景分析

    结合上述的加载原理,初始化过程可以分为如下几种场景:

    • 场景1:没有通过配置文件配置
      在这种模式下,将使用configureUsingConfiguration,此时将会使用Spring 运行时自动注入的Bean完成配置:
        protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
            Logger.Level level = getOptional(context, Logger.Level.class);
            if (level != null) {
                builder.logLevel(level);
            }
            Retryer retryer = getOptional(context, Retryer.class);
            if (retryer != null) {
                builder.retryer(retryer);
            }
            ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
            if (errorDecoder != null) {
                builder.errorDecoder(errorDecoder);
            }
            Request.Options options = getOptional(context, Request.Options.class);
            if (options != null) {
                builder.options(options);
            }
            Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
                    this.name, RequestInterceptor.class);
            if (requestInterceptors != null) {
                builder.requestInterceptors(requestInterceptors.values());
            }
    
            if (decode404) {
                builder.decode404();
            }
        }
    

    默认情况下,Spring Cloud对此超时时间的设置为:

    connectTimeoutMillis = 10 * 1000
    readTimeoutMillis = 60 * 1000
    
    • 场景2:配置了FeignClientProperties,并且配置了feign.client.defaultToProperties = true,此时的这种场景,其配置覆盖顺序如下所示:
      configureUsingConfiguration---> configurationUsingPropeties("default")----> configurationUsingProperties("<client-name>")
      如下图配置所示,最终超时时间为:connectionTimeout=4000,readTimeout=4000
    feign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
          <client-name>:
            connectTimeout: 4000
            readTimeout: 4000
    
    • 场景3:配置了FeignClientProperties,并且配置了feign.client.defaultToProperties = false,此时的这种场景,配置覆盖顺序是:
      configurationUsingPropeties("default")----> configurationUsingProperties("<client-name>")---> configureUsingConfiguration
      如果按照这种策略,则最终的超时时间设置就为connectionTimeout=10000,readTimeout=6000

    Feign的超时时间的意义:
    feign 作为最前端暴露给用户使用的,一般其超时设置相当于对用户的一个承诺,所以Spring在处理这一块的时候,会有意识地使用feign的超时时间来设置后面的ribbonhttp client组件。
    需要注意的是:hystrix的超时处理和feign之间在当前的Spring Cloud框架规划中,并没有相关关系


    2.2 Hystrix的超时设置

    Hystrix的超时设置,在于命令执行的时间,一般而言,这个时间要稍微比Feign的超时时间稍微长些,因为Command除了请求调用之外,还有一些业务代码消耗。hystrix的配置规则和feign的风格比较类似:hystrix.command.<service-name>

    hystrix.command.default.execution.isolation.strategy = THREAD
    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 10000
    hystrix.command.default.execution.timeout.enabled = true
    hystrix.command.default.execution.isolation.thread.interruptOnTimeout = true
    hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel = false
    

    Hystrix超时时间存在的意义
    Hystrix的超时时间是站在命令执行时间来看的,和Feign设置的超时时间在设置上并没有关联关系。Hystrix不仅仅可以封装Http调用,还可以封装任意的代码执行片段。Hystrix是从命令对象的角度去定义,某个命令执行的超时时间,超过此此时间,命令将会直接熔断。
    假设hystrix 的默认超时时间设置了10000,即10秒,而feign 设置的是20秒,那么Hystrix会在10秒到来是直接熔断返回,不会等到feign的20秒执行结束,也不会中断尚未执行完的feign调用。


    2.3 Ribbon 的超时时间

    Ribbon的超时时间可以通过如下配置项指定,默认情况下,这两项的值和feign的配置保持一致:

    <service-name>.ribbon.ConnectTimeout= <feign-default: 10000>
    <service-name>.ribbon.ReadTimeout= <feign-default:6000>
    

    其核心代码逻辑如下:

        IClientConfig getClientConfig(Request.Options options /*feign配置项*/, String clientName) {
            IClientConfig requestConfig;
            if (options == DEFAULT_OPTIONS) {
                requestConfig = this.clientFactory.getClientConfig(clientName);
            } else {
                requestConfig = new FeignOptionsClientConfig(options);
            }
            return requestConfig;
        }
           static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
                    //将Feign的配置设置为Ribbon的`IClientConfig`中
            public FeignOptionsClientConfig(Request.Options options) {
                setProperty(CommonClientConfigKey.ConnectTimeout,
                        options.connectTimeoutMillis());
                setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
            }
    
            @Override
            public void loadProperties(String clientName) {
    
            }
    
            @Override
            public void loadDefaultValues() {
    
            }
    
        }
    

    Ribbon超时时间存在的意义
    Ribbon的超时时间通过Feign配置项加载,构造其Ribbon客户端表示:IClientConfig,实际上该超时时间并没有实际使用的场景,仅仅作为配置项。
    由上面的原则可以看出,当feign设置了超时时间,Ribbon会依据feign的设置同步。Ribbon的这个超时时间,用于指导真正调用接口时,设置真正实现者的超时时间。

    在没有Feign的环境下,Ribbon·和·Http Client客户端的关系
    RibbonFeign是相对独立的组件,在一个Spring Cloud框架运行环境中,可以没有Feign。那么,在这种场景下,假设Http Client客户端使用的是OKHttp,并且通过ribbon.okhttp.enabled 指定ribbon调用时,会使用ribbon的超时配置来初始化OkHttp.代码如下所示:

    @Configuration
    @ConditionalOnProperty("ribbon.okhttp.enabled")
    @ConditionalOnClass(name = "okhttp3.OkHttpClient")
    public class OkHttpRibbonConfiguration {
       @RibbonClientName
       private String name = "client";
    
       @Configuration
       protected static class OkHttpClientConfiguration {
           private OkHttpClient httpClient;
    
           @Bean
           @ConditionalOnMissingBean(ConnectionPool.class)
           public ConnectionPool httpClientConnectionPool(IClientConfig config,
                                                          OkHttpClientConnectionPoolFactory connectionPoolFactory) {
                           
               RibbonProperties ribbon = RibbonProperties.from(config);
               int maxTotalConnections = ribbon.maxTotalConnections();
               long timeToLive = ribbon.poolKeepAliveTime();
               TimeUnit ttlUnit = ribbon.getPoolKeepAliveTimeUnits();
               return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
           }
    
           @Bean
           @ConditionalOnMissingBean(OkHttpClient.class)
           public OkHttpClient client(OkHttpClientFactory httpClientFactory,
                                      ConnectionPool connectionPool, IClientConfig config) {
               RibbonProperties ribbon = RibbonProperties.from(config);
               this.httpClient = httpClientFactory.createBuilder(false)
                           //使用Ribbon的超时时间来初始化OKHttp的 
                       .connectTimeout(ribbon.connectTimeout(), TimeUnit.MILLISECONDS)
                       .readTimeout(ribbon.readTimeout(), TimeUnit.MILLISECONDS)
                       .followRedirects(ribbon.isFollowRedirects())
                       .connectionPool(connectionPool)
                       .build();
               return this.httpClient;
           }
    
           @PreDestroy
           public void destroy() {
               if(httpClient != null) {
                   httpClient.dispatcher().executorService().shutdown();
                   httpClient.connectionPool().evictAll();
               }
           }
       }
    

    2.4 Http Client的超时时间

    为了保证整个组件调用链的超时关系,一般Spring Cloud采取的策略是:依赖方的超时配置覆盖被依赖方的配置
    当然这个也不是绝对的,实际上对于Feign而言,可以直接指定FeignHttpClient之间的配置关系,如下所示:

    @ConfigurationProperties(prefix = "feign.httpclient")
    public class FeignHttpClientProperties {
        public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;
        public static final int DEFAULT_MAX_CONNECTIONS = 200;
        public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;
        public static final long DEFAULT_TIME_TO_LIVE = 900L;
        public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;
        public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;
        public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;
        public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;
    
        private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;
            //连接池最大连接数,默认200
        private int maxConnections = DEFAULT_MAX_CONNECTIONS;
            //每一个IP最大占用多少连接 默认 50
        private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
            //连接池中存活时间,默认为5
        private long timeToLive = DEFAULT_TIME_TO_LIVE;
            //连接池中存活时间单位,默认为秒
        private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;
            //http请求是否允许重定向
        private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
            //默认连接超时时间:2000毫秒
        private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
            //连接池管理定时器执行频率:默认 3000毫秒
        private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
    
    }
    
    

    Http Client的实现OkHttp为例,如果指定了feign.okhttp.enabled,则会初始化Okhttp,其中,OkHttp的超时时间设置为:feign.httpclient.connectionTimeout,默认值为2000毫秒

    @Configuration
    @ConditionalOnClass(OkHttpClient.class)
    @ConditionalOnProperty(value = "feign.okhttp.enabled")
    class OkHttpFeignLoadBalancedConfiguration {
    
        @Configuration
        @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
        protected static class OkHttpFeignConfiguration {
            private okhttp3.OkHttpClient okHttpClient;
    
            @Bean
            @ConditionalOnMissingBean(ConnectionPool.class)
            public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
                                                           OkHttpClientConnectionPoolFactory connectionPoolFactory) {
                Integer maxTotalConnections = httpClientProperties.getMaxConnections();
                Long timeToLive = httpClientProperties.getTimeToLive();
                TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
                return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
            }
    
            @Bean
            public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
                                               ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
                Boolean followRedirects = httpClientProperties.isFollowRedirects();
                Integer connectTimeout = httpClientProperties.getConnectionTimeout();
                this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()).
                        connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).
                        followRedirects(followRedirects).
                        connectionPool(connectionPool).build();
                return this.okHttpClient;
            }
    
            @PreDestroy
            public void destroy() {
                if(okHttpClient != null) {
                    okHttpClient.dispatcher().executorService().shutdown();
                    okHttpClient.connectionPool().evictAll();
                }
            }
        }
    
        @Bean
        @ConditionalOnMissingBean(Client.class)
        public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                                  SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
            OkHttpClient delegate = new OkHttpClient(okHttpClient);
            return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
        }
    }
    

    3. 最佳实践

    有的同学可能觉得Spring Cloud 使用起来很方便,只需要引入一些组件即可。实际上,这正是Spring Cloud的坑所在的地方:因为它足够灵活,组件组装非常便捷,但是组件太多时,必须要有一个清晰的脉络去理清其间的关系
    在整个组件配置组装的过程,超时设置遵循的基本原则是:依赖方的超时配置覆盖被依赖方的配置,而其配置覆盖的形式,则是使用的Spring Boot 的 AutoConfiguration 机制实现的。

    综上所述,一般在Spring Cloud设置过程中,

    • 只需要指定Feign使用什么Http Client客户端即可,比如feign.okhttp.enabled=true
    • Feign客户端的Http Client的配置项,统一使用如下配置即可,Spring Cloud会拿才配置项初始化不同的Http Client客户端的。
    ### http client最大连接数,默认200
    feign.httpclient.maxConnections = 200
    ### 每个IP路由最大连接数量
    feign.httpclient.maxConnectionsPerRoute= 50
    ### 连接存活时间
    feign.httpclient.timeToLive = 900
    ### 连接存活时间单位
    feign.httpclient.timeToLiveUnit = SECONDS
    ### 连接超时时间
    feign.httpclient.connectionTimeout = 2000
    ### 连接超时定时器的执行频率
    fein.httpclient.connectionTimeout=3000
    
    • Hystrix的作用:Feign或者Http Client 只能规定所有接口调用的超时限制,而Hystrix可以设置到每一个接口的超时时间,控制力度最细,相对应地,配置会更繁琐。

    Hystrix的超时时间和Feign或者Http Client的超时时间关系
    Hystrix的超时意义是从代码执行时间层面控制超时;而FeignHttp Client 则是通过Http底层TCP/IP的偏网络层层面控制的超时。
    我的建议是:一般情况下,Hystrix 的超时时间要大于FeignHttp Client的超时时间;而对于特殊需求的接口调用上,为了避免等待时间太长,需要将对应的Hystrix command 超时时间配置的偏小一点,满足业务侧的要求。


    以上是个人对Spring Cloud使用过程中,对超时时间的理解,如果不同的见解和看法,请不吝指出,相互学习进步。

    作者声明,如需转载,请注明出处,亦山札记 https://www.jianshu.com/u/802bbe244ebb


    亦山札记,聚焦微服务、中间件

    相关文章

      网友评论

        本文标题:Spring Cloud组件那么多超时设置,如何理解和运用?

        本文链接:https://www.haomeiwen.com/subject/huurzftx.html