ホームページ >バックエンド開発 >Golang >Go のサーキット ブレーカー: カスケード障害を阻止する

Go のサーキット ブレーカー: カスケード障害を阻止する

WBOY
WBOYオリジナル
2024-07-17 17:41:111009ブラウズ

Circuit Breakers in Go: Stop Cascading Failures

サーキットブレーカー

サーキット ブレーカーは障害を検出し、障害が継続的に再発するのを防ぐ方法でそれらの障害を処理するロジックをカプセル化します。たとえば、外部サービス、データベース、または一時的に障害が発生する可能性のあるシステムの一部へのネットワーク呼び出しを処理する場合に便利です。サーキット ブレーカーを使用すると、連鎖的な障害を防止し、一時的なエラーを管理し、システム障害の中でも安定した応答性の高いシステムを維持できます。

連鎖的な障害

連鎖障害は、システムの一部の障害が他の部分の障害を引き起こし、広範囲にわたる混乱につながる場合に発生します。たとえば、分散システム内のマイクロサービスが応答しなくなり、依存するサービスがタイムアウトになり、最終的には失敗する場合があります。アプリケーションの規模によっては、これらの障害の影響は壊滅的なものになる可能性があり、パフォーマンスが低下し、おそらくユーザー エクスペリエンスに影響を与える可能性があります。

サーキットブレーカーのパターン

サーキットブレーカー自体はテクニック/パターンであり、それが動作する 3 つの異なる状態があり、それについては後で説明します。

  1. クローズ状態: クローズ状態では、サーキット ブレーカーはすべてのリクエストが通常どおりターゲット サービスに渡されることを許可します。リクエストが成功した場合、回線は閉じたままになります。ただし、障害が一定のしきい値に達すると、回路はオープン状態に移行します。これは、ユーザーが問題なくログインしてデータにアクセスできる、完全に運用可能なサービスのようなものだと考えてください。すべてが順調に進んでいます。

Circuit Breakers in Go: Stop Cascading Failures

2.オープン状態 : オープン状態では、サーキット ブレーカーはターゲット サービスへの接続を試行せずに、すべての受信リクエストを即座に失敗させます。この状態になるのは、障害が発生したサービスのさらなる過負荷を防ぎ、回復する時間を与えるためです。事前定義されたタイムアウトの後、サーキット ブレーカーは半開状態に移行します。関連のある例は次のとおりです。オンライン ストアで、すべての購入試行が失敗するという突然の問題が発生したと想像してください。システムに負荷がかかるのを避けるため、ストアは新規購入リクエストの受け付けを一時的に停止します。

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、gobreak など、この目的で使用できるライブラリがいくつかありますが、ここでは特に gobreak ライブラリの使用に焦点を当てます。

プロのヒント : gobreak パッケージの内部実装を確認できます。ここで確認してください。

外部 API への呼び出しを処理するためにサーキット ブレーカーが実装されている単純な Golang アプリケーションを考えてみましょう。この基本的な例は、サーキット ブレーカー手法を使用して外部 API 呼び出しをラップする方法を示しています。

いくつか重要なことに触れてみましょう:

  1. gobreaker.NewCircuitBreaker 関数は、カスタム設定でサーキット ブレーカーを初期化します
  2. cb.Execute メソッドは HTTP リクエストをラップし、回線の状態を自動的に管理します。
  3. MinimumRequests は、状態がハーフオープンのときに通過できるリクエストの最大数です
  4. 間隔は、サーキットブレーカーが内部カウントをクリアするための閉状態の周期です
  5. タイムアウトは、オープン状態からハーフオープン状態に移行するまでの時間です。
  6. ReadyToTrip は、クローズ状態でリクエストが失敗するたびに、カウントのコピーを使用して呼び出されます。 ReadyToTrip が true を返した場合、サーキット ブレーカーはオープン状態になります。この例では、リクエストが 3 回以上連続して失敗した場合に true を返します。
  7. OnStateChange は、サーキット ブレーカーの状態が変化するたびに呼び出されます。通常は、ここで状態変化のメトリクスを収集し、選択したメトリクス コレクターにレポートします。

サーキット ブレーカーの実装を検証するために、いくつかの単体テストを作成してみましょう。理解するために最も重要な単体テストのみを説明します。完全なコードについては、ここで確認できます。

  1. 連続して失敗したリクエストをシミュレートし、サーキット ブレーカーがオープン状態になるかどうかを確認するテストを作成します。基本的に、条件が counts.ConsecutiveFailures > であるため、3 回の障害の後、4 回目の障害が発生すると、サーキット ブレーカーがトリップ (オープン) することが予想されます。 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. オープン > をテストします。 半分 - オープン > 閉鎖状態。ただし、最初に開回路をシミュレートし、タイムアウトを呼び出します。タイムアウト後、回路をハーフオープンに移行するには、少なくとも 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 ハンドラーでわかるように、ロジックには、指定された試行回数 (試行回数 := 5) まで外部 API の呼び出しを試行するループが含まれています。試行が失敗するたびに、再試行する前に、exponentialBackoff 関数によって決定される期間待機します。

    サーキット ブレーカーの実行: サーキット ブレーカーはループ内で使用されます。外部 API 呼び出しが成功した場合 (err == nil)、ループは中断され、成功した結果が返されます。すべての試行が失敗すると、HTTP 503 (サービス利用不可) エラーが返されます。

    カスタム バックオフ戦略をサーキット ブレーカーの実装に統合することは、一時的なエラーをより適切に処理することを目的としています。再試行間の遅延が増加することで、障害が発生したサービスの負荷が軽減され、回復に時間がかかるようになります。上記のコードで明らかなように、exponentialBackoff 関数は、外部 API を呼び出すときに再試行間の遅延を追加するために導入されました。

    さらに、リアルタイム監視とアラート用の Prometheus などのツールを使用して、メトリクスとロギングを統合し、サーキット ブレーカーの状態変化を監視できます。簡単な例を次に示します:

    Go に高度な戦略を備えたサーキット ブレーカー パターンを実装する

    ご覧のとおり、次のことが完了しました:

    1. L16 ~ 21 では、リクエストの数とその状態 (成功、失敗、サーキット ブレーカーの状態変化) を追跡するために、プロメテウス カウンター ベクトルを定義します。
    2. L25 ~ 26 では、定義されたメトリクスが init 関数の Prometheus に登録されます。

    プロのヒント : Go の init 関数は、main 関数またはパッケージ内のその他のコードが実行される前にパッケージの状態を初期化するために使用されます。この場合、init 関数は requestCount メトリックを Prometheus に登録します。これにより、基本的に Prometheus がこのメトリックを認識し、アプリケーションの実行が開始されるとすぐにデータの収集を開始できるようになります。

    1. 障害カウンターを増やし、いつ回路をトリップするかを決定する ReadyToTrip 関数などのカスタム設定を使用してサーキット ブレーカーを作成します。

    2. OnStateChange は状態の変化をログに記録し、対応するプロメテウス メトリクスを増分します

    3. Prometheus メトリクスを /metrics エンドポイントで公開します

    まとめ

    この記事の締めくくりとして、回路ブレーカーが回復力と信頼性の高いシステムを構築する上でどのように大きな役割を果たすかを理解していただければ幸いです。カスケード障害を予防的に防止することで、マイクロサービスと分散システムの信頼性を強化し、逆境に直面してもシームレスなユーザー エクスペリエンスを保証します。

    スケーラビリティを考慮して設計されたシステムには、障害を適切に処理し、迅速に回復する戦略を組み込む必要があることに注意してください — Oluwafemi2024

    元々は、2024 年 6 月 7 日に https://oluwafemiakinde.dev で公開されました。

    以上がGo のサーキット ブレーカー: カスケード障害を阻止するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。