首頁 > 後端開發 > Golang > Go 中的斷路器:阻止級聯故障

Go 中的斷路器:阻止級聯故障

WBOY
發布: 2024-07-17 17:41:11
原創
1030 人瀏覽過

Circuit Breakers in Go: Stop Cascading Failures

斷路器

斷路器偵測故障並以防止故障不斷重複的方式封裝處理這些故障的邏輯。例如,它們在處理對外部服務、資料庫或系統中可能暫時失敗的任何部分的網路呼叫時非常有用。透過使用斷路器,您可以防止級聯故障、管理臨時錯誤並在系統崩潰時保持穩定且反應迅速的系統。

級聯故障

當系統某一部分的故障觸發其他部分的故障時,就會發生級聯故障,從而導致大範圍的破壞。一個例子是當分散式系統中的微服務變得無響應時,導致依賴的服務逾時並最終失敗。根據應用程式的規模,這些故障的影響可能是災難性的,這會降低效能,甚至可能影響使用者體驗。

斷路器模式

斷路器本身是一種技術/模式,它運作三種不同的狀態,我們將討論它們:

  1. 關閉狀態: 在關閉狀態下,斷路器允許所有請求正常傳遞到目標服務。如果請求成功,電路將保持關閉。然而,如果達到一定的故障閾值,電路就會轉變為開路狀態。將其視為一項完全可操作的服務,用戶可以毫無問題地登入和存取資料。一切都進展順利。

Circuit Breakers in Go: Stop Cascading Failures

2。 Open State :在開啟狀態下,斷路器立即使所有傳入請求失敗,而不嘗試聯絡目標服務。進入該狀態是為了防止故障服務進一步過載並為其提供恢復時間。在預定的超時後,斷路器進入半開狀態。一個相關的例子是這樣的;想像一下,一家線上商店突然遇到問題,每次購買嘗試都失敗。為了避免系統不堪重負,商店暫時停止接受任何新的購買請求。

Circuit Breakers in Go: Stop Cascading Failures

3。半開狀態:在半開狀態下,斷路器允許(可設定的)有限數量的測試請求傳遞到目標服務。如果這些請求成功,電路將轉換回關閉狀態。如果它們失敗,電路將返回到開路狀態。在上面我給出的處於開啟狀態的線上商店的範例中,線上商店開始允許進行幾次購買嘗試,以查看問題是否已解決。如果這幾次嘗試成功,商店將全面重新開放服務以接受新的購買請求。

此圖顯示了斷路器何時嘗試查看對 服務 B 的請求是否成功,然後失敗/中斷:

Circuit Breakers in Go: Stop Cascading Failures

後續圖顯示了對 服務 B 的測試請求成功時,電路關閉,並且所有進一步的呼叫再次路由到 服務 B

Circuit Breakers in Go: Stop Cascading Failures

注意 :斷路器的關鍵配置包括失敗閾值(開啟電路所需的失敗次數)、開啟狀態的超時時間以及半開狀態下的測試請求數量狀態。

在 Go 中實現斷路器

值得一提的是,需要具備 Go 的先驗知識才能閱讀本文。

與任何軟體工程模式一樣,斷路器可以用多種語言實現。不過,本文將重點放在 Golang 中的實作。雖然有幾個庫可用於此目的,例如 goresilience、go-resiliency 和 gobreaker,但我們將特別專注於使用 gobreaker 庫。

專業提示:您可以查看 gobreaker 套件的內部實現,請查看此處。

讓我們考慮一個簡單的 Golang 應用程序,其中實作了斷路器來處理對外部 API 的呼叫。這個基本範例示範如何使用斷路器技術包裝外部 API 呼叫:

讓我們來談談一些重要的事情:

  1. gobreaker.NewCircuitBreaker 函數使用我們的自訂設定初始化斷路器
  2. cb.Execute方法包裝HTTP請求,自動管理電路狀態。
  3. MaximumRequests 是半開狀態時允許通過的最大請求數
  4. 間隔是斷路器閉合狀態的循環週期,以清除內部計數
  5. 超時是從開啟狀態轉換到半開啟狀態之前的持續時間。
  6. 每當請求在關閉狀態下失敗時,都會使用計數副本來呼叫 ReadyToTrip。如果 ReadyToTrip 傳回 true,斷路器將進入斷開狀態。在我們的例子中,如果請求連續失敗超過三次,它將傳回 true。
  7. 每當斷路器的狀態改變時,都會呼叫 OnStateChange。您通常希望在此處收集狀態變更的指標並向您選擇的任何指標收集器報告。

讓我們寫一些單元測試來驗證我們的斷路器實作。我只會解釋最關鍵的單元測試以供理解。您可以在此處查看完整程式碼。

  1. 我們將編寫一個測試來模擬連續失敗的請求並檢查斷路器是否跳至開啟狀態。本質上,在 3 次故障之後,當第四次故障發生時,我們預計斷路器會跳閘(斷開),因為我們的條件表示 counts.ConsecutiveFailures > 3.測試如下:
 t.Run("FailedRequests", func(t *testing.T) {
         // Override callExternalAPI to simulate failure
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })
登入後複製
  1. 我們將測試開放> - 開啟> 關閉狀態。但我們首先會模擬開路並呼叫超時。超時後,我們需要至少發出一次成功請求,以使電路轉換為半開狀態。在半開狀態之後,我們需要再次成功請求電路再次完全關閉。如果出於某種原因,案例中沒有成功請求的記錄,它將恢復為開放狀態。測試如下所示:
 //Simulates the circuit breaker being open, 
 //wait for the defined timeout, 
 //then check if it closes again after a successful request.
     t.Run("RetryAfterTimeout", func(t *testing.T) {
         // Simulate circuit breaker opening
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }

         // Wait for timeout duration
         time.Sleep(settings.Timeout + 1*time.Second)

         //We expect that after the timeout period, 
         //the circuit breaker should transition to the half-open state. 

         // Restore original callExternalAPI to simulate success
         callExternalAPI = func() (int, error) {
             resp, err := http.Get(server.URL)
             if err != nil {
                 return 0, err
             }
             defer resp.Body.Close()
             return resp.StatusCode, nil
         }

         _, err := cb.Execute(func() (interface{}, error) {
             return callExternalAPI()
         })
         if err != nil {
             t.Fatalf("expected no error, got %v", err)
         }

         if cb.State() != gobreaker.StateHalfOpen {
             t.Fatalf("expected circuit breaker to be half-open, got %v", cb.State())
         }

         //After verifying the half-open state, another successful request is simulated to ensure the circuit breaker transitions back to the closed state.
         for i := 0; i < int(settings.MaxRequests); i++ {
             _, err = cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err != nil {
                 t.Fatalf("expected no error, got %v", err)
             }
         }

         if cb.State() != gobreaker.StateClosed {
             t.Fatalf("expected circuit breaker to be closed, got %v", cb.State())
         }
     })
登入後複製
  1. 讓我們測試一下在連續 2 次失敗請求後觸發的 ReadyToTrip 條件。我們將有一個變數來追蹤連續失敗。 ReadyToTrip 回呼已更新,以檢查斷路器是否在 2 次故障後跳脫(counts.ConsecutiveFailures > 2)。我們將編寫一個測試來模擬故障並驗證計數以及斷路器在指定的故障次數後是否轉換為開啟狀態。
 t.Run("ReadyToTrip", func(t *testing.T) {
         failures := 0
         settings.ReadyToTrip = func(counts gobreaker.Counts) bool {
             failures = int(counts.ConsecutiveFailures)
             return counts.ConsecutiveFailures > 2 // Trip after 2 failures
         }

         cb = gobreaker.NewCircuitBreaker(settings)

         // Simulate failures
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }
         for i := 0; i < 3; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if failures != 3 {
             t.Fatalf("expected 3 consecutive failures, got %d", failures)
         }
         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })
登入後複製

進階策略

我們可以更進一步,在斷路器實作中加入指數退避策略。我們將透過示範指數退避策略的範例來保持本文的簡單和簡潔。然而,還有其他值得一提的高階斷路器策略,例如減載、隔離、回退機制、上下文和取消。這些策略基本上增強了斷路器的穩健性和功能性。這是使用指數退避策略的範例:

指數退避

具有指數退避功能的斷路器

讓我們澄清幾件事:

自訂退避函數:exponentialBackoff 函數實現具有抖動的指數退避策略。它基本上根據嘗試次數計算退避時間,確保延遲隨著每次重試嘗試呈指數增長。

處理重試: 如您在 /api 處理程序中看到的,邏輯現在包含一個循環,該循環嘗試呼叫外部 API 最多指定的嘗試次數( attempts := 5)。每次嘗試失敗後,我們都會等待exponentialBackoff函數決定的持續時間,然後再重試。

斷路器執行:斷路器在循環內使用。如果外部API呼叫成功(err == nil),則循環中斷,並傳回成功結果。如果所有嘗試都失敗,則會傳回 HTTP 503(服務不可用)錯誤。

在斷路器實作中整合自訂退避策略確實旨在更優雅地處理瞬態錯誤。重試之間不斷增加的延遲有助於減少失敗服務的負載,讓它們有時間恢復。如同上面的程式碼所示,我們引入了exponentialBackoff函數來在呼叫外部API時增加重試之間的延遲。

此外,我們可以使用 Prometheus 等工具整合指標和日誌記錄來監控斷路器狀態變化,以進行即時監控和警報。這是一個簡單的例子:

在 go 中使用進階策略實現斷路器模式

如您所見,我們現在已經完成了以下操作:

  1. 在 L16-21 中,我們定義了一個 prometheus 計數器向量來追蹤請求的數量及其狀態(成功、失敗、斷路器狀態變化)。
  2. 在 L25-26 中,定義的指標在 init 函數中註冊到 Prometheus。

專業提示:Go 中的 init 函數用於在執行 main 函數或套件中的任何其他程式碼之前初始化套件的狀態。在本例中,init 函數向 Prometheus 註冊 requestCount 指標。這基本上確保了 Prometheus 知道這個指標,並且可以在應用程式開始運行後立即開始收集數據。

  1. 我們使用自訂設定來建立斷路器,包括 ReadyToTrip 功能,可增加故障計數器並確定何時使電路跳脫。

  2. OnStateChange 記錄狀態變更並增加對應的 prometheus 指標

  3. 我們在 /metrics 端點公開 Prometheus 指標

總結

作為本文的總結,我希望您看到斷路器如何在建立有彈性且可靠的系統中發揮巨大作用。透過主動防止級聯故障,它們增強了微服務和分散式系統的可靠性,即使在逆境中也能確保無縫的使用者體驗。

請記住,任何為可擴展性而設計的系統都必須採用策略來優雅地處理故障并快速恢復 — Oluwafemi2024

原發表於 https://oluwafemiakinde.dev 於 2024 年 6 月 7 日。

以上是Go 中的斷路器:阻止級聯故障的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板