概念:
- HTTP調用,應用層走的HTTP協議,但網絡層面始終是TCP/IP協議。TCP/IP是面向連接的協議,在傳輸數據之前需要建立連接。幾乎所有網絡框架都會提供兩個超時參數。
- :建立TCP連接的時間;確認需要明白連接的是誰。
連接超時:ConnectTomeout
- 時間不易過長:讓用戶配置建連階段的最長等待時間,一般來說,TCP三次握手建立連接需要的時間非常短,通常在毫秒級最多至秒級,因此,設置(1~9秒)即可。如果是純內內網用的話,這個參數可以更短,在下游服務離線無法連接的時候,可以快速失敗。
- 排查服務端/Nginx:通常情況下,客戶端負載均衡來連服務端,會直接建立連接,此時出現連接超時大概率是服務端的問題;而如果服務端通過類似Nginx反向代理來負載均衡,客戶端連接的其實是Nginx,而不是服務端,此時連接超時排查Nginx。
讀取超時:ReadTimeout
- 等待遠端返回數據的時間,包括遠端程序處理的時間;解決需要考慮下游服務的服務標准和自己的服務標准,設置合適的讀取超時時間。
- 重試:因為HTTP協議中GET請求表示數據查詢操作,無狀態,並且考慮到常見的網絡丟包現象,HTTP客戶端或代理服務器會自動重試GET/HEAD請求。
- 注意:
- 讀取超時時,只要服務端收到請求,網絡層面的超時和斷開不會影響服務端的執行。因此,出現讀取超時不能隨意假設服務端的處理情況,需要根據業務狀態考慮如何進行后續處理。
- 讀取超時是數據傳輸的最長耗時+服務端處理業務邏輯的時間。
- 讀取超時設置根據實際情況,對於定時任務和異步任務,超時配置長些問題不大。微服務中短平快的同步接口調用,並發較大,應設置一個較短的讀取超時時間,防止被下游服務拖慢,通常不會設置超過30秒。過長會讓下游抖動影響到自己,過短可能影響成功率。
超時參數調整:
- ①連接超時與讀取超時在微服務中同時配置才有效
- ②如果接口設計不支持冥等,需要關閉自動重試,更好的解決方案是遵從HTTP協議使用合適的HTTP方法
- ③包括HttpClient在內的HTTP客戶端及瀏覽器,都會限制客戶端調用的最大並發數。所以在客戶端有比較大的請求調用並發,需要調正默認值,防止達到吞吐量的瓶頸。
SpringCloud之Feign超時的配置:
- Feign有兩個超時參數,它使用的負載均衡組件Ribbon也有配置,需要注意以下幾點
- 一、Feign的讀取超時默認1秒
-
//RibbonClientConfiguration實現-創建出來默認設置1s /** * 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; @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; }
//修改默認配置 feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000
-
- 二、讀取超時和連接超時需要同時配置才能生效
-
//FeignClientFactoryBean if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); }
//針對單獨的Feign Client設置超時時間,把default替換為Client的name feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000 feign.client.config.clientsdk.readTimeout=2000 feign.client.config.clientsdk.connectTimeout=2000
-
- 三、單獨的超時可以覆蓋全局超時
- 四、通過配置Ribbon組件的參數修改兩個超時時間
-
ribbon.ReadTimeout=4000 ribbon.ConnectTimeout=4000
-
- 五、同時配置Feign和Ribbon,以Feign為准
-
//LoadBalancerFeignClient //options不是默認值,創建FieignOptionsClientConfig,Ribbon的配置被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; }
-
Ribbon自動重試導致的短信重復發送問題:
- 出現場景:用戶服務調用短信服務,代碼中沒有重試邏輯。
- 還原邏輯:一次Get請求發送短信接口,客戶端調用用戶服務,用戶服務通過feign調用短信服務(Feign內部有一個Ribbon組件負責客戶端負載均衡,通過配置文件設置其調用的服務端為兩個節點,可以驗證ribbon的一次重試)
- 解決:
- 一、把發送短信接口從Get改為Post,有狀態的API接口不應該定義為Get,根據HTTP協議的規范,Get請求用於數據查詢,選擇Get還是Post的依據,應該是API的行為,而不是參數大小。
- 二、將MaxAutoRetiresNextServer參數配置為0,禁用服務調用失敗后在下一個服務端節點自動重試。
-
ribbon.MaxAutoRetriesNextServer=0
- 源碼:
-
// DefaultClientConfigImpl public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;//由此可知Get請求在某個服務端節點出現問題(如讀取超時)時,Ribbon會自動重試一次 public static final int DEFAULT_MAX_AUTO_RETRIES = 0; // RibbonLoadBalancedRetryPolicy public boolean canRetry(LoadBalancedRetryContext context) { HttpMethod method = context.getRequest().getMethod(); return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations(); } @Override public boolean canRetrySameServer(LoadBalancedRetryContext context) { return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context); } @Override public boolean canRetryNextServer(LoadBalancedRetryContext context) { // this will be called after a failure occurs and we increment the counter // so we check that the count is less than or equals to too make sure // we try the next server the right number of times return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context); }
-