斷路器偵測故障並以防止故障不斷重複的方式封裝處理這些故障的邏輯。例如,它們在處理對外部服務、資料庫或系統中可能暫時失敗的任何部分的網路呼叫時非常有用。透過使用斷路器,您可以防止級聯故障、管理臨時錯誤並在系統崩潰時保持穩定且反應迅速的系統。
當系統某一部分的故障觸發其他部分的故障時,就會發生級聯故障,從而導致大範圍的破壞。一個例子是當分散式系統中的微服務變得無響應時,導致依賴的服務逾時並最終失敗。根據應用程式的規模,這些故障的影響可能是災難性的,這會降低效能,甚至可能影響使用者體驗。
斷路器本身是一種技術/模式,它運作三種不同的狀態,我們將討論它們:
2。 Open State:在開啟狀態下,斷路器立即使所有傳入請求失敗,而不嘗試聯絡目標服務。進入該狀態是為了防止故障服務進一步過載並為其提供恢復時間。在預定的超時後,斷路器進入半開狀態。一個相關的例子是這樣的;想像一下,一家線上商店突然遇到問題,每次購買嘗試都失敗。為了避免系統不堪重負,商店暫時停止接受任何新的購買請求。
3。半開狀態:在半開狀態下,斷路器允許(可設定的)有限數量的測試請求傳遞到目標服務。如果這些請求成功,電路將轉換回關閉狀態。如果它們失敗,電路將返回到開路狀態。在上面我給出的處於開啟狀態的線上商店的範例中,線上商店開始允許進行幾次購買嘗試,以查看問題是否已解決。如果這幾次嘗試成功,商店將全面重新開放服務以接受新的購買請求。
此圖顯示了斷路器何時嘗試查看對服務 B的請求是否成功,然後失敗/中斷:
後續圖顯示了對服務 B的測試請求成功時,電路關閉,並且所有進一步的呼叫再次路由到服務 B:
注意:斷路器的關鍵配置包括失敗閾值(開啟電路所需的失敗次數)、開啟狀態的超時時間以及半開狀態下的測試請求數量狀態。
值得一提的是,需要具備 Go 的先驗知識才能閱讀本文。
與任何軟體工程模式一樣,斷路器可以用多種語言實現。不過,本文將重點放在 Golang 中的實作。雖然有幾個庫可用於此目的,例如 goresilience、go-resiliency 和 gobreaker,但我們將特別專注於使用 gobreaker 庫。
專業提示:您可以查看 gobreaker 套件的內部實現,請查看此處。
讓我們考慮一個簡單的 Golang 應用程序,其中實作了斷路器來處理對外部 API 的呼叫。這個基本範例示範如何使用斷路器技術包裝外部 API 呼叫:
讓我們來談談一些重要的事情:
讓我們寫一些單元測試來驗證我們的斷路器實作。我只會解釋最關鍵的單元測試以供理解。您可以在此處查看完整程式碼。
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()) } })
//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()) } })
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 中使用進階策略實現斷路器模式
如您所見,我們現在已經完成了以下操作:
專業提示:Go 中的 init 函數用於在執行 main 函數或套件中的任何其他程式碼之前初始化套件的狀態。在本例中,init 函數向 Prometheus 註冊 requestCount 指標。這基本上確保了 Prometheus 知道這個指標,並且可以在應用程式開始運行後立即開始收集數據。
我們使用自訂設定來建立斷路器,包括 ReadyToTrip 功能,可增加故障計數器並確定何時使電路跳脫。
OnStateChange 記錄狀態變更並增加對應的 prometheus 指標
我們在 /metrics 端點公開 Prometheus 指標
作為本文的總結,我希望您看到斷路器如何在建立有彈性且可靠的系統中發揮巨大作用。透過主動防止級聯故障,它們增強了微服務和分散式系統的可靠性,即使在逆境中也能確保無縫的使用者體驗。
請記住,任何為可擴展性而設計的系統都必須採用策略來優雅地處理故障并快速恢復 — Oluwafemi,2024
原發表於https://oluwafemiakinde.dev於 2024 年 6 月 7 日。
以上是Go 中的斷路器:阻止級聯故障的詳細內容。更多資訊請關注PHP中文網其他相關文章!