- 原文地址:Retries, Timeouts and Backoff
- 原文做者:namc
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:nettee
- 校對者:fireairforce
分佈式系統很難。即便咱們學了不少構建高可用性系統的方法,也經常會忽略系統設計中的彈性(resiliency)。html
咱們確定據說過容錯性,但什麼是「彈性」呢?我的而言,我喜歡將其定義爲系統處理意外狀況並最終從中恢復的能力。有不少方法使你的系統能從故障中回彈,但在這篇文章中,咱們主要關注如下幾點:前端
簡單來講,超時就是兩個連續的數據包之間的最大不活動時間。mysql
假設咱們在某個時刻已經使用過了數據庫驅動和 HTTP 客戶端。全部幫助你的服務鏈接到一個外部服務器的客戶端或驅動都有 Timeout 參數。這個參數一般默認爲零或 -1,表示超時時間未定義,或是無限時間。android
例如:參考 connectTimeout
和 socketTimeout
的定義 Mysql Connector 配置ios
大多數對外部服務器的請求都附有一個超時時間。當外部服務器沒有及時響應時,超時的設置很是有必要。若是沒有設置超時,並使用默認值 0/-1,你的程序可能會阻塞幾分鐘或更長的時間。這是由於,當你沒有收到來自應用服務器的響應,而且你的超時時間無限或很是大時,這個鏈接會一直開着。隨着有更多的請求到來,更多的鏈接會打開,並永遠沒法關閉。這會致使你的鏈接池耗盡,進而致使你的應用的故障。git
那麼,每當你使用這樣的鏈接器來配置你的應用時,請務必在配置中設置顯式的超時值。github
超時必須在前端和後端中都實現。若是一個讀/寫操做在一個 REST API 或 socket 接口上阻塞了太長時間,它應當拋出異常,而且斷開鏈接。這能夠通知後端取消操做並關閉鏈接,從而防止鏈接始終打開。sql
咱們可能須要瞭解瞬時故障這個術語,由於咱們後面會頻繁用到它。簡單地說,服務中的瞬時故障是一種暫時的失靈,例如網絡擁塞,數據庫過載,是一種在有足夠的冷卻週期以後也許能本身恢復的故障。數據庫
如何判斷一個故障是不是瞬時的?後端
答案取決於你的 API/Server 響應的實現細節。若是你有一個 REST API,請返回 503 Service Unavailable,而不是其餘 5xx/4xx 錯誤碼。這可讓客戶端知道超時是由「臨時的過載」引發的,而不是因爲代碼層面的錯誤。
重試雖然有用,但若是沒有正確地配置,則會讓人討厭。下面闡述瞭如何找出正確的重試方法。
重試
若是從服務器收到的錯誤是瞬時的,例如網絡數據包在傳輸時損壞,應用程序能夠當即重試請求,由於故障不太可能再次發生。
然而,這種方法很是激進。若是你的服務已經滿負荷運行,或是已經徹底不可用,這種方法可能對你的服務有害。這種方法還會拖慢應用的響應時間,由於你的服務會嘗試不斷執行一個失敗的操做。
若是你的業務邏輯須要這樣的重試策略,你最好限制重試的次數,不向同一個源頭髮送過多的請求。
帶延遲的重試
若是是鏈接失敗或網絡上的過大流量致使的故障,應用程序則應當根據業務邏輯,在重試請求以前添加延遲時間。
for(int attempts = 0; attempts < 5; attempts++)
{
try
{
DoWork();
break;
}
catch { }
Thread.Sleep(50); // 延遲
}
複製代碼
當使用一個鏈接至外部服務的庫時,請檢查它是否實現了重試策略,容許你配置重試的最大次數、重試之間的延遲等。
你還能夠經過設置 Retry-After 響應頭,在服務器端實現重試的策略。
用日誌記錄操做失敗的緣由也很重要。有時候操做失敗是由於缺乏資源,這能夠經過添加更多的服務實例來解決。也有時候操做失敗多是由於內存泄漏或空指針異常。那麼,添加日誌跟蹤你的應用程序的行爲就很重要了。
如上所述,咱們能夠向重試策略中添加延遲。這種延遲一般稱爲線性退避。這可能不是實現一個重試策略的最佳方法。
考慮這種狀況:你的服務由於數據庫的過載發生了故障。咱們的請求極可能在幾回重試以後會成功。但不斷髮送的請求也可能加劇你的數據庫服務器的過載問題。所以,數據庫服務會在過載狀態停留更長時間,也會須要更多的時間從過載狀態中恢復。
有幾種策略能夠用於解決這個問題。
1. 指數退避
顧名思義,指數退避不是在重試之間進行週期性的延遲(例如 5 秒),而是指數性地增長延遲時間。重試會一直進行到最大次數限制。若是請求始終失敗,就告訴客戶端請求失敗了。
你還必須設置最大延遲時間的限制。指數退避可能致使出現很是大的延遲時間,致使請求的 socket 保持無限期開啓,並使線程「永遠」休眠。這會耗盡系統資源,致使鏈接池的更多問題。
int delay = 50
for(int attempts = 0; attempts < 5; attempts++)
{
try
{
DoWork();
break;
}
catch { }
Thread.sleep(delay);
if (delay < MAX_DELAY) // MAX_DELAY 可能依賴於應用程序和業務邏輯
{
delay *= 2;
}
}
複製代碼
指數退避在分佈式系統中的一個主要缺點是,在同一時間開始退避的請求,也會在同一時間進行重試。這致使了請求簇的出現。那麼,咱們並無減小每一輪進行競爭的客戶端數量,而是引入了沒有客戶端競爭的時期。固定的指數退避並不能減小不少競爭,並會生成負載峯值。
2. 帶抖動的退避
爲了處理指數退避的負載峯值問題,咱們向退避策略中添加抖動。抖動是一種去相關性策略,在重試的間隔中添加隨機性,從而分攤了負載,避免了出現網絡請求簇。
抖動一般不是任何一項配置屬性,須要客戶端來實現。抖動所須要的只是一個能夠加入隨機性的函數,能夠在重試以前動態地計算出等待的時間。
引入抖動以後,最初的一組失敗的請求可能彙集在一個很小的窗口中,例如 100 ms。可是在每一個重試周期以後,請求簇會攤開到愈來愈大的時間窗口中。當請求分攤在足夠大的窗口上時,服務就極可能可以處理這些請求。
int delay = 50
for(int attempts = 0; attempts < 5; attempts++)
{
try
{
DoWork();
break;
}
catch { }
Thread.sleep(delay);
delay *= random.randrange(0, min(MAX_DELAY, delay * 2 ** i)) // 只是生成一個簡單的隨機數
}
複製代碼
在長時間的瞬時故障的狀況下,任何的重試可能都不是最好的方法。這種故障多是因爲鏈接失效,電力中斷(是的,很是真實的狀況)致使的。客戶端最終會重試若干次,浪費了系統資源,並進一步致使了更多系統中的故障。
那麼,咱們須要一種能夠肯定故障是否會長期持續的機制,並實現一種應對該狀況的解決方案。
3. 斷路器
斷路器模式在處理服務的長時間瞬時故障時很是有用。它經過肯定服務的可用性,防止客戶端重試註定會失敗的請求。
斷路器設計模式要求在一系列的請求中保留鏈接的狀態。讓咱們看看 failsafe 實現的斷路器
CircuitBreaker breaker = new CircuitBreaker()
.withFailureThreshold(5)
.withSuccessThreshold(3)
.withDelay(1, TimeUnit.MINUTES);
Failsafe.with(breaker).run(() -> connect());
複製代碼
當一切正常運行時,沒有故障,斷路器保持在關閉狀態。
當達到執行故障的閾值時,斷路器跳閘並進入打開狀態。這意味着,後續的全部請求會直接失敗,不會通過重試的邏輯。
通過一段延遲以後(如上述設置的 1 分鐘),斷路器會進入半開狀態,測試網絡請求的問題是否依然存在,並決定斷路器是應當關閉仍是打開。若是請求成功,斷路器會重置爲關閉狀態,不然會從新置爲打開狀態。
這有助於在長時間的故障中避免重試執行的彙集,節省系統資源。
雖然斷路器能夠用一個狀態變量在本地維護。可是若是你有一個分佈式系統,你可能須要一個外部存儲層。在多節點的配置中,應用服務器的狀態須要在多個實例之間共享。在這種場景下,你可使用 Redis、memcached 來記錄外部服務的可用性。在向外部服務發送任何請求以前,從持久存儲中查詢服務的狀態。
冪等的服務是指客戶端能夠重複地發起相同的請求,並獲得相同的最終結果。雖然服務器會對此操做產生相同的結果,但客戶端不必定做出相同的反應。
對於 REST API 而言,你須要記住 ——
爲何關注冪等操做呢?
在分佈式系統中,有多個服務器和客戶端節點。若是你從客戶端向服務器 A 發送了請求,請求失敗或超時了,那麼你想可以簡單地再次發送該請求,而沒必要擔憂先前的請求是否有任何反作用。
這在微服務中是極其重要的,由於有不少獨立工做的組件。
冪等性的一些主要好處有 ——
咱們梳理了一系列構建更容錯系統的方法。然而,這些方法並非所有。最後,我想指出幾個供你查看的要點,或許能幫助提升你係統的可用性和容錯性。
一些資源:
感謝!❤
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。