前言
Spring Cloud
作為微服務(wù)解決方案 全家桶逛球,集合了豐富的微服務(wù)組件,如Gateway
苫昌、Feign
颤绕、Hystrix
,Ribbon
、OkHttp
祟身、Eureka
等等奥务。而作為服務(wù)調(diào)用環(huán)節(jié)涉及到的幾個(gè)組件:Feign
、Hystrix
,Ribbon
袜硫、OkHttp
都有超時(shí)時(shí)間的設(shè)置氯葬,Spring Cloud 是如何優(yōu)雅地把它們協(xié)調(diào)好呢?本文將為你揭曉答案婉陷。
1. Spring Cloud 中發(fā)起一個(gè)接口調(diào)用帚称,經(jīng)過(guò)了哪些環(huán)節(jié)?
Spring Cloud 在接口調(diào)用上秽澳,大致會(huì)經(jīng)過(guò)如下幾個(gè)組件配合:
Feign
-----> Hystrix
--->Ribbon
---> Http Client(apache http components 或者 Okhttp)
具體交互流程上闯睹,如下圖所示:
-
接口化請(qǐng)求調(diào)用
當(dāng)調(diào)用被@FeignClient
注解修飾的接口時(shí),在框架內(nèi)部肝集,會(huì)將請(qǐng)求轉(zhuǎn)換成Feign的請(qǐng)求實(shí)例feign.Request
瞻坝,然后交由Feign框架處理蛛壳。 -
Feign :轉(zhuǎn)化請(qǐng)求
至于Feign的詳細(xì)設(shè)計(jì)和實(shí)現(xiàn)原理杏瞻,在此不做詳細(xì)說(shuō)明。
請(qǐng)參考我的另外一篇文章:Spring Cloud Feign 設(shè)計(jì)原理 -
Hystrix :熔斷處理機(jī)制
Feign的調(diào)用關(guān)系衙荐,會(huì)被Hystrix代理攔截捞挥,對(duì)每一個(gè)Feign調(diào)用請(qǐng)求,Hystrix都會(huì)將其包裝成HystrixCommand
,參與Hystrix的流控和熔斷規(guī)則忧吟。如果請(qǐng)求判斷需要熔斷砌函,則Hystrix直接熔斷,拋出異沉镒澹或者使用FallbackFactory
返回熔斷Fallback
結(jié)果讹俊;如果通過(guò),則將調(diào)用請(qǐng)求傳遞給Ribbon
組件煌抒。
關(guān)于Hystrix的工作原理仍劈,參考Spring Cloud Hystrix設(shè)計(jì)原理 -
Ribbon :服務(wù)地址選擇
當(dāng)請(qǐng)求傳遞到Ribbon
之后,Ribbon
會(huì)根據(jù)自身維護(hù)的服務(wù)列表,根據(jù)服務(wù)的服務(wù)質(zhì)量寡壮,如平均響應(yīng)時(shí)間贩疙,Load等讹弯,結(jié)合特定的規(guī)則,從列表中挑選合適的服務(wù)實(shí)例这溅,選擇好機(jī)器之后组民,然后將機(jī)器實(shí)例的信息請(qǐng)求傳遞給Http Client
客戶端,HttpClient
客戶端來(lái)執(zhí)行真正的Http接口調(diào)用悲靴;
關(guān)于Ribobn的工作原理臭胜,參考Spring Cloud Ribbon設(shè)計(jì)原理 -
HttpClient :Http客戶端,真正執(zhí)行Http調(diào)用
根據(jù)上層Ribbon
傳遞過(guò)來(lái)的請(qǐng)求癞尚,已經(jīng)指定了服務(wù)地址庇楞,則HttpClient開(kāi)始執(zhí)行真正的Http請(qǐng)求。
關(guān)于HttpClient的其中一個(gè)實(shí)現(xiàn)OkHttp
的工作原理否纬,請(qǐng)參考Spring Cloud OkHttp設(shè)計(jì)原理
2.每個(gè)組件階段的超時(shí)設(shè)置
如上一章節(jié)展示的調(diào)用關(guān)系吕晌,每個(gè)組件自己有獨(dú)立的接口調(diào)用超時(shí)設(shè)置參數(shù),下面將按照從上到下的順序梳理:
2.1 feign的默認(rèn)配置
feign 的配置可以采用feign.client.config.<feginName>....
的格式為每個(gè)feign客戶端配置临燃,對(duì)于默認(rèn)值睛驳,可以使用feign.client.config.default..
的方式進(jìn)行配置,該配置項(xiàng)在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
其中乏沸,關(guān)于feign
的管理連接超時(shí)的配置項(xiàng):
## 網(wǎng)絡(luò)連接時(shí)間
feign.client.config.<clientname>.connectTimeout=
## 讀超時(shí)時(shí)間
feign.client.config.<clientname>.readTimeout=
2.2 Spring Cloud 加載feign配置項(xiàng)的原理:
- 檢查是否Feign是否制定了上述的配置項(xiàng),即是否有
FeignClientProperties
實(shí)例爪瓜;- 如果有上述的配置項(xiàng)蹬跃,則表明
Feign
是通過(guò)properties
初始化的,即configureUsingProperties
;- 根據(jù)配置項(xiàng)
feign.client.defaultToProperties
的結(jié)果,使用不同的配置覆蓋策略铆铆。
feign
初始化的過(guò)程蝶缀,其實(shí)就是構(gòu)造Feign.Builder
的過(guò)程,如下圖所示:
相關(guān)代碼實(shí)現(xiàn)如下:
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.場(chǎng)景分析
結(jié)合上述的加載原理薄货,初始化過(guò)程可以分為如下幾種場(chǎng)景:
- 場(chǎng)景1:沒(méi)有通過(guò)配置文件配置
在這種模式下翁都,將使用configureUsingConfiguration
,此時(shí)將會(huì)使用Spring 運(yùn)行時(shí)自動(dòng)注入的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();
}
}
默認(rèn)情況下,Spring Cloud對(duì)此超時(shí)時(shí)間的設(shè)置為:
connectTimeoutMillis = 10 * 1000
readTimeoutMillis = 60 * 1000
- 場(chǎng)景2:配置了
FeignClientProperties
,并且配置了feign.client.defaultToProperties = true
,此時(shí)的這種場(chǎng)景谅猾,其配置覆蓋順序如下所示:
configureUsingConfiguration
--->configurationUsingPropeties("default")
---->configurationUsingProperties("<client-name>")
如下圖配置所示柄慰,最終超時(shí)時(shí)間為:connectionTimeout=4000
,readTimeout=4000
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
<client-name>:
connectTimeout: 4000
readTimeout: 4000
- 場(chǎng)景3:配置了
FeignClientProperties
,并且配置了feign.client.defaultToProperties = false
,此時(shí)的這種場(chǎng)景,配置覆蓋順序是:
configurationUsingPropeties("default")
---->configurationUsingProperties("<client-name>")
--->configureUsingConfiguration
如果按照這種策略税娜,則最終的超時(shí)時(shí)間設(shè)置就為connectionTimeout=10000
,readTimeout=6000
Feign的超時(shí)時(shí)間的意義:
feign 作為最前端暴露給用戶使用的坐搔,一般其超時(shí)設(shè)置相當(dāng)于對(duì)用戶的一個(gè)承諾,所以Spring在處理這一塊的時(shí)候敬矩,會(huì)有意識(shí)地使用feign的超時(shí)時(shí)間來(lái)設(shè)置后面的ribbon
和http client
組件概行。
需要注意的是:hystrix
的超時(shí)處理和feign
之間在當(dāng)前的Spring Cloud
框架規(guī)劃中,并沒(méi)有相關(guān)關(guān)系谤绳。
2.2 Hystrix的超時(shí)設(shè)置
Hystrix的超時(shí)設(shè)置占锯,在于命令執(zhí)行的時(shí)間袒哥,一般而言,這個(gè)時(shí)間要稍微比Feign的超時(shí)時(shí)間稍微長(zhǎng)些,因?yàn)镃ommand除了請(qǐng)求調(diào)用之外消略,還有一些業(yè)務(wù)代碼消耗堡称。hystrix的配置規(guī)則和feign的風(fēng)格比較類似: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超時(shí)時(shí)間存在的意義
Hystrix的超時(shí)時(shí)間是站在命令執(zhí)行時(shí)間來(lái)看的,和Feign設(shè)置的超時(shí)時(shí)間在設(shè)置上并沒(méi)有關(guān)聯(lián)關(guān)系艺演。Hystrix不僅僅可以封裝Http調(diào)用却紧,還可以封裝任意的代碼執(zhí)行片段。Hystrix是從命令對(duì)象
的角度去定義胎撤,某個(gè)命令執(zhí)行的超時(shí)時(shí)間晓殊,超過(guò)此此時(shí)間,命令將會(huì)直接熔斷伤提。
假設(shè)hystrix 的默認(rèn)超時(shí)時(shí)間設(shè)置了10000
,即10秒
巫俺,而feign 設(shè)置的是20秒
,那么Hystrix
會(huì)在10秒到來(lái)是直接熔斷返回肿男,不會(huì)等到feign
的20秒執(zhí)行結(jié)束介汹,也不會(huì)中斷尚未執(zhí)行完的feign
調(diào)用。
2.3 Ribbon 的超時(shí)時(shí)間
Ribbon的超時(shí)時(shí)間可以通過(guò)如下配置項(xiàng)指定舶沛,默認(rèn)情況下嘹承,這兩項(xiàng)的值和feign的配置保持一致:
<service-name>.ribbon.ConnectTimeout= <feign-default: 10000>
<service-name>.ribbon.ReadTimeout= <feign-default:6000>
其核心代碼邏輯如下:
IClientConfig getClientConfig(Request.Options options /*feign配置項(xiàng)*/, 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的配置設(shè)置為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超時(shí)時(shí)間存在的意義
Ribbon
的超時(shí)時(shí)間通過(guò)Feign
配置項(xiàng)加載,構(gòu)造其Ribbon
客戶端表示:IClientConfig
,實(shí)際上該超時(shí)時(shí)間并沒(méi)有實(shí)際使用的場(chǎng)景,僅僅作為配置項(xiàng)如庭。
由上面的原則可以看出叹卷,當(dāng)feign
設(shè)置了超時(shí)時(shí)間,Ribbon
會(huì)依據(jù)feign
的設(shè)置同步坪它。Ribbon的這個(gè)超時(shí)時(shí)間骤竹,用于指導(dǎo)真正調(diào)用接口時(shí),設(shè)置真正實(shí)現(xiàn)者的超時(shí)時(shí)間哟楷。
在沒(méi)有
Feign
的環(huán)境下瘤载,Ribbon·和·Http Client
客戶端的關(guān)系
Ribbon
和Feign
是相對(duì)獨(dú)立的組件,在一個(gè)Spring Cloud框架運(yùn)行環(huán)境中卖擅,可以沒(méi)有Feign。那么墨技,在這種場(chǎng)景下惩阶,假設(shè)Http Client
客戶端使用的是OKHttp
,并且通過(guò)ribbon.okhttp.enabled
指定ribbon
調(diào)用時(shí)扣汪,會(huì)使用ribbon
的超時(shí)配置來(lái)初始化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的超時(shí)時(shí)間來(lái)初始化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的超時(shí)時(shí)間
為了保證整個(gè)組件調(diào)用鏈的超時(shí)關(guān)系断楷,一般Spring Cloud采取的策略是:依賴方
的超時(shí)配置覆蓋被依賴方
的配置
當(dāng)然這個(gè)也不是絕對(duì)的,實(shí)際上對(duì)于Feign
而言崭别,可以直接指定Feign
和HttpClient
之間的配置關(guān)系冬筒,如下所示:
@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;
//連接池最大連接數(shù)恐锣,默認(rèn)200
private int maxConnections = DEFAULT_MAX_CONNECTIONS;
//每一個(gè)IP最大占用多少連接 默認(rèn) 50
private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
//連接池中存活時(shí)間,默認(rèn)為5
private long timeToLive = DEFAULT_TIME_TO_LIVE;
//連接池中存活時(shí)間單位舞痰,默認(rèn)為秒
private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;
//http請(qǐng)求是否允許重定向
private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
//默認(rèn)連接超時(shí)時(shí)間:2000毫秒
private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
//連接池管理定時(shí)器執(zhí)行頻率:默認(rèn) 3000毫秒
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
}
以 Http Client
的實(shí)現(xiàn)OkHttp
為例土榴,如果指定了feign.okhttp.enabled
,則會(huì)初始化Okhttp
,其中,OkHttp的超時(shí)時(shí)間設(shè)置為:feign.httpclient.connectionTimeout
,默認(rèn)值為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. 最佳實(shí)踐
有的同學(xué)可能覺(jué)得
Spring Cloud
使用起來(lái)很方便,只需要引入一些組件即可响牛。實(shí)際上玷禽,這正是Spring Cloud
的坑所在的地方:因?yàn)樗銐蜢`活,組件組裝非常便捷呀打,但是組件太多時(shí)矢赁,必須要有一個(gè)清晰的脈絡(luò)去理清其間的關(guān)系。
在整個(gè)組件配置組裝的過(guò)程贬丛,超時(shí)設(shè)置遵循的基本原則是:依賴方
的超時(shí)配置覆蓋被依賴方
的配置撩银,而其配置覆蓋的形式,則是使用的Spring Boot 的AutoConfiguration
機(jī)制實(shí)現(xiàn)的豺憔。
綜上所述蜒蕾,一般在Spring Cloud設(shè)置過(guò)程中,
- 只需要指定Feign使用什么
Http Client
客戶端即可焕阿,比如feign.okhttp.enabled=true
- Feign客戶端的
Http Client
的配置項(xiàng)咪啡,統(tǒng)一使用如下配置即可,Spring Cloud
會(huì)拿才配置項(xiàng)初始化不同的Http Client
客戶端的暮屡。
### http client最大連接數(shù)撤摸,默認(rèn)200
feign.httpclient.maxConnections = 200
### 每個(gè)IP路由最大連接數(shù)量
feign.httpclient.maxConnectionsPerRoute= 50
### 連接存活時(shí)間
feign.httpclient.timeToLive = 900
### 連接存活時(shí)間單位
feign.httpclient.timeToLiveUnit = SECONDS
### 連接超時(shí)時(shí)間
feign.httpclient.connectionTimeout = 2000
### 連接超時(shí)定時(shí)器的執(zhí)行頻率
fein.httpclient.connectionTimeout=3000
- Hystrix的作用:
Feign
或者Http Client
只能規(guī)定所有接口調(diào)用的超時(shí)限制,而Hystrix
可以設(shè)置到每一個(gè)接口的超時(shí)時(shí)間褒纲,控制力度最細(xì)准夷,相對(duì)應(yīng)地,配置會(huì)更繁瑣莺掠。
Hystrix的超時(shí)時(shí)間和Feign或者
Http Client
的超時(shí)時(shí)間關(guān)系
Hystrix的超時(shí)意義是從代碼執(zhí)行時(shí)間層面控制超時(shí)衫嵌;而Feign
或Http Client
則是通過(guò)Http底層TCP/IP的偏網(wǎng)絡(luò)層層面控制的超時(shí)。
我的建議是:一般情況下彻秆,Hystrix 的超時(shí)時(shí)間要大于Feign
或Http Client
的超時(shí)時(shí)間楔绞;而對(duì)于特殊需求的接口調(diào)用上,為了避免等待時(shí)間太長(zhǎng)唇兑,需要將對(duì)應(yīng)的Hystrix command 超時(shí)時(shí)間配置的偏小一點(diǎn)酒朵,滿足業(yè)務(wù)側(cè)的要求。
以上是個(gè)人對(duì)Spring Cloud
使用過(guò)程中扎附,對(duì)超時(shí)時(shí)間的理解蔫耽,如果不同的見(jiàn)解和看法,請(qǐng)不吝指出留夜,相互學(xué)習(xí)進(jìn)步匙铡。
作者聲明图甜,如需轉(zhuǎn)載,請(qǐng)注明出處鳖眼,亦山札記 http://www.reibang.com/u/802bbe244ebb