先點贊再看,養成好習慣
在網絡請求中,因爲網絡是不可靠的,因此常常會有請求失敗的場景。針對這種問題,一般的作法是增長重試機制,在請求失敗後從新請求,儘可能保證請求的成功,從而提升服務的穩定性。java
但是大多數人不肯意輕易的重試,由於每每重試會帶來更大的風險。好比過多的重試,會給被調用服務形成更大的壓力,放大原有的問題。git
以下圖所示,服務 A 調用 服務 B,服務 B 根據請求數據不一樣,會調用 服務 C 和 服務 D。此時服務 C 出現故障,不可用了,那麼服務 B 中全部對服務 C 的請求都會超時,但服務 D 如今仍是可用的;可因爲服務 A 中大量的重試致使服務 B 的負載快速升高,很快的將服務 B 的負載打滿(好比鏈接池沾滿)。如今調用服務 D 的分支請求也不可用了,由於服務 B 已經被重試請求打滿,沒法再處理任何請求了。github
若是服務自身是可用的,但網絡出現較大的延遲、抖動或者丟包,致使請求到達目標服務或返回發起服務超時;此時若是客戶端發起重試,那麼對於接收端來講,極可能會收到多個相同的請求。因此服務端還須要增長冪等的處理,保證屢次請求下結果一致apache
既然重試有風險,那難道就不該該重試嗎?失敗就直接失敗,啥都無論嗎?後端
是否進行重試,這個須要區分當前失敗的緣由,不能簡單粗暴的決定重試或者不重試。網絡很複雜,鏈路很長,不一樣類型的協議,決定是否重試的策略也有所不一樣。瀏覽器
一個基本的 HTTP 請求,會包含如下幾個階段:安全
在 DNS 解析階段時,若是域名不存在,或者域名沒有 DNS 記錄,根據域名沒法解析到對應的主機地址列表,那麼根本就沒法發起請求,此時重試沒有任何意義,因此並不須要重試網絡
在 TCP 握手階段,若是目標服務不可用,那麼此時重試也沒有什麼意義,由於在請求的第一步- 握手都不成功,大機率這個 host 是不可用的。app
挺過了 DNS 和握手兩個階段以後,終於到了收發數據的階段。到了這一步一旦出現失敗,是否重試要考慮的因素可就更多了。負載均衡
以下圖所示的這種狀況中,由於網絡擁塞等緣由,致使數據到達服務端時間過長,但最終服務端也收到了完整的報文,已經開始處理請求,可此時客戶端由於超時放棄了該請求,那麼若是客戶端此時新建一條 TCP 鏈接發起重試,那麼對於服務端來講就會收到兩次相同的請求報文,處理兩次該請求,可能形成嚴重的後果
因此這種已經發送成功的狀況,就不適合重試
問題來了,怎麼樣才能知道我發送成功了呢?socket.write沒有報錯就算成功了?SocketChannel.write以後,Buffer 寫空了就算成功了?
並無那麼簡單,應用層的 socket write,只是將數據寫入 SND Buffer 中,至於 SND Buffer 中的數據何時被操做系統發送至網絡,這個並無任何保證。阻塞和非阻塞也只是針對 socket.write 這個操做的,當 SND Buffer已滿,沒法將數據寫入內核 SND Buffer 時,就會發生阻塞。
但咱們能夠粗略的認爲,socket.write 成功 而且 應用層 buffer 被寫空,就是已經發送成功了。
如今看看另外一種狀況,當數據發送時對端就直接關閉了socket,返回 rst 標識:
那麼這種狀況,就很適合進行重試。由於對於服務端來講,並無開始處理這個請求,因此重試(重建鏈接重發請求)只會提升可用性,並不會形成什麼負擔
HTTP 協議中,對 Request Method 還有一些語義上的約定:
GET | POST | PUT | DELET |
---|---|---|---|
列出URI,以及該資源組中每一個資源的詳細信息(後者可選)。 | 在本組資源中建立/追加一個新的資源。該操做每每返回新資源的URL。 | 使用給定的一組資源替換當前整組資源。 | 刪除整組資源。 |
安全(更是冪等) | 非冪等 | 冪等 | 冪等 |
PUT/DELETE 是冪等操做,因此就算處理相同報文的請求也不會有數據重複之類的問題。但 POST 可不是,POST 的語義是建立/添加,這是一個非冪等的請求類型。
如今回到上面重試的問題,若是請求報文已經發送成功,但響應超時,但由請求的 API Method 是一個DELETE 類型,這種狀況就能夠考慮重試,由於 DELETE 語義上是冪等的;GET/PUT 同理,語義上冪等的就能夠考慮重試。
但 POST 可不行,由於語義上是非冪等的,重試極可能形成重複的處理請求
但是……一切真的那麼美好嗎?能嚴格準守語義的 API 能有幾家?因此單靠語義上的約定,很是不穩妥,必定要足夠了解服務端的接口是否支持冪等,才能夠考慮重試問題。
HTTPS 面世這麼多年,終於在近幾年徹底普及了,沒升級的網站在瀏覽器中都會提示不安全,目前能暴露在公網的 Web API 也基本都是上 HTTPS 的。
在 HTTPS 中,重試的策略又會有一些變化:
上圖是HTTPS 握手的流程,在 TCP 創建鏈接以後,會先進行 SSL 的握手,驗證對端證書,生成臨時對稱密鑰之列的操做。
若是在 SSL 握手階段就發生失敗,好比證書到期,證書不受信等問題,那麼也是徹底不須要重試的。由於這種問題不會是短暫的,一旦出現就是長時間失敗,重試也是失敗。
介紹完了 HTTP(S) 協議下對重試的考慮,如今來看看主流網絡庫對重試的處理方式,看看這種主流開源項目中的處理機制夠不夠「合理」
Apache HttpClient 是 Java 裏最主流的一個 HTTP 工具庫了(後端方向),雖然 JDK 也提供了基本的 HTTP SDK,但……太基礎了,無法直接使用。而 Apache HttpClient(簡稱Apache HC)彌補了這個不足,提供了一套超級強大的 HTTP SDK,功能強大、使用簡單、全部組件均可以定製。
Apache HC 默認的重試策略類在org.apache.http.impl.client.DefaultHttpRequestRetryHandler
,先來看看實現(省略了一些不重要的代碼):
//返回true,表明須要重試,false不重試 @Override public boolean retryRequest( final IOException exception, final int executionCount, final HttpContext context) { //判斷重試次數是否達到上線 if (executionCount > this.retryCount) { // Do not retry if over max retry count return false; } //判斷哪些異常不用重試 if (this.nonRetriableClasses.contains(exception.getClass())) { return false; } //判斷是不是冪等請求 if (handleAsIdempotent(request)) { // Retry if the request is considered idempotent return true; } //請求報文是否已經發送 if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) { // Retry if the request has not been sent fully or // if it's OK to retry methods that have been sent return true; } // otherwise do not retry return false; }
簡單總結下 Apache HC的重試策略:
判斷哪些異常不須要重試
這樣看起來,Apache HC 中默認的重試策略,和咱們上一節介紹的「合理的」重試策略徹底一致。因而可知這種主流的開源項目真的很優秀,質量很是高,全部設計都按照標準來,拿這種項目的源碼當學習資料更能事半功倍。
Dubbo 中重試機制的代碼在com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker
(2.7之後包名更新爲 org.apache.dubbo)
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { //獲取配置的重試次數,默認1即不重試 int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1; Set<String> providers = new HashSet<String>(len); for (int i = 0; i < len; i++) { Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked); invoked.add(invoker); RpcContext.getContext().setInvokers((List) invoked); try { Result result = invoker.invoke(invocation); if (le != null && logger.isWarnEnabled()) { logger.warn("Although retry the method " + invocation.getMethodName() + " in the service " + getInterface().getName() + " was successful by the provider " + invoker.getUrl().getAddress() + ", but there have been failed providers " + providers + " (" + providers.size() + "/" + copyinvokers.size() + ") from the registry " + directory.getUrl().getAddress() + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " + Version.getVersion() + ". Last error is: " + le.getMessage(), le); } return result; } catch (RpcException e) { //Biz類型的異常,會拋出異常,不進行不重試,非Biz類的RpcException都會進行重試 if (e.isBiz()) { // biz exception. throw e; } le = e; } catch (Throwable e) { le = new RpcException(e.getMessage(), e); } finally { providers.add(invoker.getUrl().getAddress()); } } }
從代碼中能夠看出,只有不是 Biz 類型的 RpcException,纔會觸發重試。繼續分析代碼看看什麼場景會觸發重試……算了不貼代碼了,直接上答案!
簡單總結一下 Dubbo 中的重試策略:
Dubbo 的重試策略仍是有一些激進的,並無像 Apache HC 那樣謹慎……因此在使用 Dubbo 時,重試策略必定要當心,避免重試到一些不支持冪等的服務。若是你的 provider 不支持冪等,最好將重試次數配置爲 0
Feign 是一個使用簡單 Java 的 Http 客戶端,也是 Spring Cloud 中推薦的 RPC 框架。雖然 Feign 也屬於 Http 客戶端,但它和 Apache HC 之類的庫相比卻有很大的不一樣。
下面 Feign 的核心結構圖,從圖上能夠看到,Feign 的客戶端部分,除了支持 JDK 內置的 Http Client 之外,還支持 Apache HC,Google Http,OK Http 等 Http 庫。
並且還提有 encoders/decoder 的抽象……這麼看起來,它並不能算是一款基礎的 Http 客戶端,更應該稱爲 「Http 工具」?或者叫一個 RPC 的基礎抽象?
那 Feign 中的重試策略是怎麼樣的呢?這個問題實在很差回答,由於要區分不少狀況,在不一樣的 Feign Client 下,重試策略都有所不一樣
首先,Feign 是有內置的重試策略的,以下圖所示,Feign 的重試在調用 HttpClient 以外,並且每次重試以前有必定間隔。
默認配置下,最大重試5次(包括第一次),每次重試前會間隔必定時間(sleep),並且這個每次間隔時間是隨着重試次數的增長而遞增的,重試間隔計算公式爲:
$$重試間隔 = 重試間隔(默認100ms) * 1.5 ^ {當前重試次數 -1}$$
以下圖所示,重試的次數越大,每次的重試間隔就會越長
但這是在調用 HttpClient 以外的重試,若是隻是使用 Feign 內置默認 JDK HTTP Client 的話也不會出什麼問題,由於JDK HTTP Client 很簡單,沒有重試機制,單靠 Feign 的重試機制就足夠了。
但是在配合三方 Http Client (好比 Apache HC)時,狀況就不太同樣了,由於三方的 Http Client 內部每每是有重試機制的。
若是三方的 HttpClient 有重試,Feign 又有重試,那麼至關於重試了兩層,重試次數變成了 N * N
好比在 Apache HC 下,按照前面的介紹,默認重試3次,而 Feign 默認重試5次,那麼最壞的狀況下,重試次數高達 15 次。
並且這還只是 Feign 在基本用法下的重試機制,若是在 Spring Cloud 下,配合 Ribbon 之類的負載均衡器,狀況會更復雜一些,本文就不過多介紹了,有興趣的看看 Spring Cloud 下 Feign 的配置
重試雖然看着很簡單,但若是想安全穩定的重試,要考慮的因素仍是不少的。必定要結合當前業務場景,上下文信息去綜合考慮是否應該重試和每次重試的次數;而不是一拍腦殼就定一個重試機制,暴力重試每每只會放大問題,帶來更嚴重的後果。
若是不肯定是否能夠安全的重試,那麼就不要重試,禁用這些框架的重試,Failfast 總比問題擴大更好。
原創不易,禁止未受權的轉載。若是個人文章對您有幫助,就請點贊/收藏/關注鼓勵支持一下吧❤❤❤❤❤❤