1. 引言
在spring cloud微服中,feign远程调用可能是大家每天都接触到东西,但很多同学却没咋搞清楚这里边的各种超时问题,生产环境可能会蹦出各种奇怪的问题。
首先说下结论:
1)只使用feign组件,不使用ribbion组件,其默认feign的连接超时是10s,读超时是60s;
2) 同时使用了feign和ribbion组件,(1)若没有任何人为配置超时时间,远程调用使用ribbion的默认超时时间,连接超时、读超时都是1秒钟;(2)若同时主动配置了feign 、ribbion的超时时间,则使用配置的feign超时时间;(3)若只主动配置了feign的超时时间,则使用配置的feign超时时间;(4)若只主动配置了ribbon超时时间,则使用配置的ribbion超时时间。
2. only feign
feign本身是通过FeignClientFactoryBean
创建出来的,feign的超时配置也在方法org.springframework.cloud.openfeign.FeignClientFactoryBean#configureUsingConfiguration
中。此方法properties.isDefaultToProperties()
的默认值是true,我们一般也不会去改它,所以一般是执行红方框中的逻辑。(1)先执行全局配置configureUsingConfiguration(context, builder)
,(2)再执行属性配置类中FeignClientProperties
的默认配置, (3)最后执行属性配置类FeignClientProperties
中当前feign client特定的配置。这三个步骤的覆盖顺序是后者覆盖前者,也就是越往后优先级越高。默认情况下我们未做任何配置时,FeignClientProperties
是空对象,也就是说此时只会执行步骤(1)
的属性配置。
全局属性配置方法org.springframework.cloud.openfeign.FeignClientFactoryBean#configureUsingConfiguration
回到spring容器中去取超时配置对象Request.Options
自动配置类相应方法为我们配置了一个Request.Options
对象(当spring容器中不存在此类型的bean时),
而LoadBalancerFeignClient.DEFAULT_OPTIONS
的连接超时、读超时分别是10s、60s.
public class LoadBalancerFeignClient implements Client {
static final Request.Options DEFAULT_OPTIONS = new Request.Options();
//.....
}
public static class Options {
private final long connectTimeout;
private final TimeUnit connectTimeoutUnit;
private final long readTimeout;
private final TimeUnit readTimeoutUnit;
private final boolean followRedirects;
//.......
/**
* Creates the new Options instance using the following defaults:
* <ul>
* <li>Connect Timeout: 10 seconds</li>
* <li>Read Timeout: 60 seconds</li>
* <li>Follow all 3xx redirects</li>
* </ul>
*/
public Options() {
this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
}
//.......
}
3. feign+ribbon
在feign引入ribbon负载均衡时,远程调用的feign client会用LoadBalancerFeignClient
.
在LoadBalancerFeignClient
方法中有调用getClientConfig(options, clientName);
的代码行,而IClientConfig
中包含有feign和ribbion超时时间。
getClientConfig方法参数中的options
是feign本身的超时配置,
可以看到当参数options
等于DEFAULT_OPTIONS
,即没有为feign主动配置超时时间这种情况,这种情况会到容器用去获取超时配置;当options
不等于DEFAULT_OPTIONS
,即已经为feign主动配置了超时时间这种情况下,我们会使用feign的超时时间。
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
这个IClientConfig
在配置类org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration
的配置方法ribbonClientConfig
执行config.loadProperties(this.name);
将配置文件中ribbion的配置信息赋值到config对象后,又执行了config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
、 config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
重新将连接超时、读超时重置为1000毫秒,即1秒。
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
// Order is important here, last should be the default, first should be optional
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
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";
@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;
}
//....
}
}
我们在看看DefaultClientConfigImpl的怎么处理属性的。
可以看出上边RibbonClientConfiguration.ribbonClientConfig
方法中设置的超时时间,它们被放在DefaultClientConfigImpl#properties中的,而取配置属性的底层方法都是getProperty(String key)
,所以在没有为feign配置超时时间时,ribbon的取值时先取当前指定ribbon client的超时时间,若不存在则取全局ribbon的超时时间,若还是不存在,则取DefaultClientConfigImpl#properties中的默认值1秒。
public class DefaultClientConfigImpl implements IClientConfig {
@Override
public <T> DefaultClientConfigImpl set(IClientConfigKey<T> key, T value) {
properties.put(key.key(), value);
return this;
}
protected Object getProperty(String key) {
//这里enableDynamicProperties一定是true
if (enableDynamicProperties) {
String dynamicValue = null;
DynamicStringProperty dynamicProperty = dynamicProperties.get(key);
//dynamicProperty是null
if (dynamicProperty != null) {
dynamicValue = dynamicProperty.get();
}
if (dynamicValue == null) {
//到配置文件中取当前client的'clientName.ribbion.ReadTimeout'这种格式的属性
dynamicValue = DynamicProperty.getInstance(getConfigKey(key)).getString();
if (dynamicValue == null) {
//到配置文件中取全局'ribbion.ReadTimeout'这种格式的属性
dynamicValue = DynamicProperty.getInstance(getDefaultPropName(key)).getString();
}
}
if (dynamicValue != null) {
return dynamicValue;
}
}
//DefaultClientConfigImpl类中定义的默认值
return properties.get(key);
}
}
LoadBalancerFeignClient#execute
方法中的lbClient(clientName)
创建了一个负载均衡器RetryableFeignLoadBalancer
,且此负载均衡期中的config就是上边RibbonClientConfiguration#ribbonClientConfig
配置方法中所对应的IClientConfig
.
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName);
if (client != null) {
return client;
}
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
ServerIntrospector.class);
client = this.loadBalancedRetryFactory != null
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
this.loadBalancedRetryFactory)
: new FeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client);
return client;
}
现在回到LoadBalancerFeignClient#execute
方法块调用负载均衡的
return lbClient(clientName) .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
的核心实现RetryableFeignLoadBalancer#execute
FeignLoadBalancer是RetryableFeignLoadBalancer的父类型,构造RetryableFeignLoadBalancer时调用的构造方法new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory)
会调用父类的public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, ServerIntrospector serverIntrospector)
方法,而RibbonClientConfiguration#ribbonClientConfig
配置方法中所对应的IClientConfig
会传入此构造方法的clientConfig参数,因此这里的this.connectTimeout
this.readTimeout
都是ribbon的超时时间。
configOverride
是feign超时配置或ribbon超时配置,而ribbon.connectTimeout(this.connectTimeout)
中this.connectTime
超时参数是作为默认值的,所以说ribbon超时参数是会被覆盖掉,它的优先级低(feign、ribbon超时参数同时存在时)