[譯] 分佈式系統如何從故障中恢復?— 重試、超時和退避

重試、超時和退避

分佈式系統很難。即便咱們學了不少構建高可用性系統的方法,也經常會忽略系統設計中的彈性(resiliency)。html

咱們確定據說過容錯性,但什麼是「彈性」呢?我的而言,我喜歡將其定義爲系統處理意外狀況並最終從中恢復的能力。有不少方法使你的系統能從故障中回彈,但在這篇文章中,咱們主要關注如下幾點:前端

超時

簡單來講,超時就是兩個連續的數據包之間的最大不活動時間。mysql

假設咱們在某個時刻已經使用過了數據庫驅動和 HTTP 客戶端。全部幫助你的服務鏈接到一個外部服務器的客戶端或驅動都有 Timeout 參數。這個參數一般默認爲零或 -1,表示超時時間未定義,或是無限時間。android

例如:參考 connectTimeoutsocketTimeout 的定義 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 而言,你須要記住 ——

  • POST 不是冪等的 —— POST 致使在服務器上建立新資源。n 個 POST 請求會在服務器上建立 n 個新的資源。
  • GETHEADOPTIONSTRACE 方法永遠不會改變服務器上資源的狀態。所以,它們老是冪等的。
  • PUT 請求是冪等的。n 個 PUT 請求會覆蓋相同的資源 n-1 次。
  • DELETE 是冪等的,由於它一開始會返回 200(OK),然後續的調用會返回 204(No Content)或 404(Not Found)。

爲何關注冪等操做呢?

在分佈式系統中,有多個服務器和客戶端節點。若是你從客戶端向服務器 A 發送了請求,請求失敗或超時了,那麼你想可以簡單地再次發送該請求,而沒必要擔憂先前的請求是否有任何反作用。

這在微服務中是極其重要的,由於有不少獨立工做的組件。

冪等性的一些主要好處有 ——

  • 最小的複雜性 —— 不須要擔憂反作用,能夠簡單地重試任何請求,並獲得相同的最終結果。
  • 易於實現 —— 你不須要添加邏輯來處理你的重試機制中先前失敗的請求。
  • 易於測試 —— 每一個動做都會產生相同的結果,沒有意外。

結語

咱們梳理了一系列構建更容錯系統的方法。然而,這些方法並非所有。最後,我想指出幾個供你查看的要點,或許能幫助提升你係統的可用性和容錯性。

  • 在多節點配置中,若是一個客戶端重試了屢次,這些請求極可能到達同一個服務器。此時,最好返回一個失敗的響應,讓客戶端從頭重試。
  • 對你的系統作性能統計,讓它們時刻準備最壞的狀況。你能夠查看 Netflix 的 Chaos Monkey —— 這是一個在系統中觸發隨機故障的彈性測試工具。這能讓你爲可能發生的故障作好準備,構建一個有彈性的系統。
  • 若是你的系統因爲某種緣由處於過載狀態,你能夠嘗試經過減載(load shedding)來分佈負載。Google 作了一個很棒的案例研究,能夠做爲一個很好的起點。

一些資源:

感謝!❤

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索