Spring Cloud組件那麼多超時設置,如何理解和運用?

前言

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的過程,如下圖所示:

在這裏插入圖片描述
相關代碼實現如下:

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://louluan.blog.csdn.net
另外作者已開通微信訂閱號,精品文章同步更新,歡迎關注~
亦山札記公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章