前言
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 超時時間配置的偏小一點,滿足業務側的要求。
以上是個人對Spring Cloud
使用過程中,對超時時間的理解,如果不同的見解和看法,請不吝指出,相互學習進步。
作者聲明,如需轉載,請註明出處,亦山札記 https://louluan.blog.csdn.net
另外作者已開通微信訂閱號,精品文章同步更新,歡迎關注~