前言
Spring Cloud
作为微服务解决方案 全家桶,集合了丰富的微服务组件,如Gateway
、Feign
、Hystrix
,Ribbon
、OkHttp
、Eureka
等等。而作为服务调用环节涉及到的几个组件:Feign
、Hystrix
,Ribbon
、OkHttp
都有超时时间的设置,Spring Cloud 是如何优雅地把它们协调好呢?本文将为你揭晓答案。
1. Spring Cloud 中发起一个接口调用,经过了哪些环节?
Spring Cloud 在接口调用上,大致会经过如下几个组件配合: Feign
-----> Hystrix
—>Ribbon
—> Http Client(apache http components 或者 Okhttp)
具体交互流程上,如下图所示:
- 接口化请求调用 当调用被
@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配置项的原理:
- 检查是否Feign是否制定了上述的配置项,即是否有
FeignClientProperties
实例; - 如果有上述的配置项,则表明
Feign
是通过properties
初始化的,即configureUsingProperties
; - 根据配置项
feign.client.defaultToProperties
的结果,使用不同的配置覆盖策略。
feign
初始化的过程,其实就是构造Feign.Builder
的过程,如下图所示:
相关代码实现如下:
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的超时时间来设置后面的
ribbon
和http 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
客户端的关系Ribbon
和Feign
是相对独立的组件,在一个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
而言,可以直接指定Feign
和HttpClient
之间的配置关系,如下所示:
@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的超时意义是从代码执行时间层面控制超时;而Feign
或Http Client
则是通过Http底层TCP/IP的偏网络层层面控制的超时。 我的建议是:一般情况下,Hystrix 的超时时间要大于Feign
或Http Client
的超时时间;而对于特殊需求的接口调用上,为了避免等待时间太长,需要将对应的Hystrix command 超时时间配置的偏小一点,满足业务侧的要求。