Goにおけるサーキットブレーカーパターン:カスケード障害の防止

Goマイクロサービスにおけるカスケード障害を防止する

目次

サーキットブレーカーは、失敗している依存サービスへのリクエストを遮断し、ゴルーチン、ソケット、メモリを消費して最終的にシステム全体が崩壊するというカスケード障害を防ぎます。

難しいのは状態マシンそのものではありません。問題は、どこにブレーカーを設置するか、何を失敗とみなすか、タイムアウトやリトライとどう相互作用するか、そしてサーキットが開いた(オープン)際にサービスがどう振る舞うべきか、という判断です。

circuit breaker

Go言語におけるサーキットブレーカーパターンは、特に外部呼び出し(アウトバウンドコール)において非常に有用です。HTTP API、決済ゲートウェイ、検索サービス、メールプロバイダー、LLMゲートウェイ、内部マイクロサービスなど、遅延や過負荷、部分的な利用不能に陥りうる依存リソースに対して適用されます。適切に使用すれば、カスケード障害を軽減できます。しかし誤用すると、単なる不明瞭な障害モードになるだけです。

サーキットブレーカーが解決する問題とは?

分散システムは、クリーンに失敗することは稀です。

依存リソースが完全にダウンしているわけではありません。以下のような状態である可能性があります。

  • 500エラーを返している
  • 429(レート制限)レスポンスを返している
  • TCP接続は受け付けるが、応答しない
  • 300ミリ秒ではなく30秒も応答に要している
  • 一部のリクエストのみで失敗している
  • すべてのクライアントが同時にリトライしているため過負荷になっている

最悪のケースは、通常はハードな障害(完全な停止)ではありません。それは「遅い依存リソース」です。

遅い呼び出しは、ゴルーチン、ソケット、データベース接続、メモリ、およびワーカーのキャパシティを消費します。すでに不健康な依存リソースを待ち続けると、あなたのサービス自体も不健康な状態に陥る可能性があります。

サーキットブレーカーは、依存リソースが失敗閾値を超えた時点で迅速に失敗(フェイルファスト)することで、これを防ぎます。

以下のような状態を永遠に続ける代わりに:

リクエスト -> 依存リソース呼び出し -> 待機 -> タイムアウト -> リトライ -> 待機 -> 失敗

サービスは最終的に以下のような状態になります:

リクエスト -> サーキットオープン -> フォールバックまたはエラーを即時返却

この迅速な失敗は必ずしも快適ではありませんが、予測可能です。予測可能な失敗は、ゆっくりとした崩壊よりも運用しやすくなります。

サーキットブレーカーの3つの状態

ほとんどのサーキットブレーカーは3つの状態を使用します。

クローズド(Closed)

サーキットは、通常の運用中はクローズド(閉じている)状態です。

リクエストは通過を許可されます。ブレーカーは成功と失敗を記録します。失敗の数または比率が閾値を超えると、ブレーカーはオープン(開く)状態に移行します。

クローズドであることは「永遠に安全」という意味ではありません。「現在、トラフィックが許可されている」ことを意味します。

オープン(Open)

依存リソースが不健康とみなされると、サーキットはオープン(開いている)状態になります。

リクエストは直ちに拒否されます。サービスはフォールバック、キャッシュされたレスポンス、劣化されたレスポンス、または明確なアップストリームエラーを返すべきです。

オープン状態は依存リソースを修復するわけではありません。依存リソースが回復する時間を提供し、呼び出し側が無駄なリソースを消費するのを保護します。

ハーフオープン(Half-Open)

クールダウン期間の後、ブレーカーはハーフオープン(半開)状態になります。

限られた数の試行リクエストのみが通過を許可されます。それらが成功すれば、ブレーカーはクローズドになります。失敗すれば、再びオープンになります。

ハーフオープンは、以下の2つの悪い極端な状態を避けるために重要です。

  • 依存リソースを二度と試さないこと
  • 完全にトラフィックを戻しすぎること

状態遷移は以下のようになります。

stateDiagram-v2 [*] --> Closed Closed --> Open: Failure threshold reached Open --> HalfOpen: Timeout elapsed HalfOpen --> Closed: Trial succeeds HalfOpen --> Open: Trial fails

サーキットブレーカー vs タイムアウト vs リトライ

一般的な間違いとして、サーキットブレーカー、リトライ、タイムアウトを相互交換可能なものとして扱うことがあります。これらは関連していますが、異なる問題を解決します。

タイムアウト

タイムアウトは、1つの操作が実行され得る最大時間を制限します。

Go言語では、通常、デッドラインまたはタイムアウトを持つ context.Context を外部呼び出しに渡します。

タイムアウトは以下の質問に答えます:

この1つの呼び出しに対して、どれほど待つことを許容するか?

リトライ

リトライは、一時的な失敗の可能性のある操作を再試行します。

リトライは、短時間のネットワークグリッチ、一時的な503レスポンス、接続のリセット、その他の一過性の障害に対して有用です。

リトライは以下の質問に答えます:

この呼び出しを再試行すべきか?

サーキットブレーカー

サーキットブレーカーは、依存リソースがおそらく不健康であると判断した場合に呼び出しを停止します。

それは以下の質問に答えます:

今、この依存リソースを呼び出すべきか?

レートリミッター

レートリミッターは、時間内に許可されるトラフィックの量を制御します。

それは以下の質問に答えます:

この呼び出し側はどれだけのトラフィックを送るべきか?

バルクヘッド

バルクヘッドはリソースを分離し、1つの依存リソースがすべてを消費するのを防ぎます。

それは以下の質問に答えます:

この依存リソースは私のサービスをどれほど損傷し得るか?

これらのパターンは、組み合わせることで最も強くなります。タイムアウトのないサーキットブレーカーは弱いです。ジッターのないリトライはリトライストームを引き起こす可能性があります。メトリクスのないフォールバックは障害を隠す可能性があります。

Go言語でサーキットブレーカーを使用すべきタイミング

あなたのサービスが、あなたのサービスとは独立して失敗し得る依存リソースを呼び出す場合に、サーキットブレーカーを使用します。

良い候補には以下が含まれます:

  • 外部HTTP API
  • 決済プロセッサ
  • メールおよびSMSプロバイダー
  • 検索サービス
  • 推薦サービス
  • LLM推論ゲートウェイ
  • 内部マイクロサービスエンドポイント
  • サードパーティSaaS API
  • 遅いまたは過負荷の読み取り側サービス

呼び出し側がグレースフルに劣化できる場合に、サーキットブレーカーは特に有用です。

例えば:

  • キャッシュされた製品データを返す
  • 推薦ブロックをスキップする
  • 決済プロバイダーを一時的に利用不可としてマークする
  • 作業を後でキューイングする
  • 部分的なレスポンスを返す
  • 明確な一時的エラーで迅速に失敗する

重要な質問は「この呼び出しは失敗し得るか?」ではありません。すべては失敗し得ます。より良い質問は以下の通りです:

この依存リソースが失敗している場合、それにフルトラフィックを送り続けるべきか?

答えが「いいえ」であれば、サーキットブレーカーが役立つかもしれません。

サーキットブレーカーを使用すべきでない場合

パターンが責任あるように聞こえるからといって、すべての関数にサーキットブレーカーを追加しないでください。

サーキットブレーカーは通常、以下には有用ではありません:

  • ローカルなプロセス内関数呼び出し
  • モノリス内の単純なCRUD操作
  • バリデーションロジック
  • 決定論的なビジネスルール
  • CPU専用のローカル操作
  • 有用なフォールバックが存在しないコードパス
  • 冪等でない書き込み操作
  • より強固なワークフローレイヤーですでに保護されている依存リソース

サーキットブレーカーはまた、基本的な衛生措置を置き換えるものではありません:

  • タイムアウトを設定する
  • コンテキストを伝播する
  • 接続プールを正しく使用する
  • エラーを明示的に処理する
  • リトライを安全にする
  • 失敗率を観測する

悪いサーキットブレーカーは、システムを理解しにくくします。真の問題を隠し、トラフィックを過剰に拒否し、回復中に混乱した振る舞いを生む可能性があります。

少し意見的なルールはシンプルです:

サーキットブレーカーは至る所ではなく、依存リソースの境界に追加する。

Goサーキットブレーカーライブラリの選択

基本的なサーキットブレーカーは自分で実装できますが、本番環境のGoサービスではライブラリを使用するのが一般的です。

最も一般的でシンプルな選択肢は sony/gobreaker です。

これにより以下が得られます:

  • クローズド、オープン、ハーフオープンの状態
  • 設定可能な失敗閾値
  • 設定可能なオープン状態のタイムアウト
  • 状態変更コールバック
  • リクエストカウンター
  • v2でのジェネリックサポート
  • 小さなAPIサーフェス

より大規模なレジリエンスパイプラインには、リトライ、タイムアウト、フォールバック、レート制限、バルクヘッド分離、サーキットブレーキングなど、複数のポリシーを組み合わせるライブラリも検討してください。これは、単一の操作の周りに単一のレジリエンスレイヤーを構築したい場合に有用です。

しかし、多くのGoサービスにとって、gobreaker で十分です。

Goサーキットブレーカーパッケージの比較

Goの標準ライブラリには組み込みのサーキットブレーカーは含まれていません。実践的には、小さなサーキットブレーカーライブラリ、大規模なレジレンスフレームワーク、または古いHystrixスタイルのパッケージのいずれかを選択します。

新しいGoサービスのほとんどにおいて、決定はシンプルです:

  • 小型で焦点の絞られたサーキットブレーカーが欲しい場合は sony/gobreaker を使用する
  • リトライ、タイムアウト、フォールバック、バルクヘッド、レート制限、その他のレジレンスポリシーと組み合わせたサーキットブレーカーが欲しい場合は failsafe-go を使用する
  • すでにそれを使用するレガシーコードがない限り、hystrix-go で新規プロジェクトを開始するのは避ける
パッケージ 最適用途 強み トレードオフ
sony/gobreaker/v2 HTTP/RPCクライアント周りのシンプルなサーキットブレーカー 小さなAPI、ジェネリックv2サポート、明確な状態モデル、依存クライアントのラップが容易 サーキットブレーキングのみを解決;リトライ、タイムアウト、フォールバックは別途組み合わせる必要がある
failsafe-go 完全なレジレンスポリシーの組み合わせ リトライ、フォールバック、サーキットブレーカー、タイムアウト、バルクヘッド、レートリミッター、キャッシュ、ヘッジ、アダプティブリミッター、アダプティブスロットラーポリシー 学ぶべき概念が多い;基本的なブレーカーのみが必要な場合は重すぎる
afex/hystrix-go レガシーHystrixスタイルのシステム 馴染み深いHystrixの概念、コマンドスタイルの実行、歴史的な使用実績 古い設計;新しいGoサービスには最適なデフォルトではない
go-kit/kit/circuitbreaker Go kitのエンドポイントベースのサービス Go kitのミドルウェアスタイルとエンドポイントアーキテクチャに適合 サービスがすでにGo kitを使用している場合にのみ有用
cep21/circuit Hystrixのようなサーキットブレーカーの振る舞い より機能豊富なHystrixスタイルのアプローチ シンプルなデフォルトとしてあまり一般的でない;小さなサービスには必要以上かもしれない

私のデフォルトの推奨は意図的に地味です:サーキットブレーカーのみが必要な場合は sony/gobreaker/v2 から始めます。1つの場所で完全なレジレンスポリシーを表現したい場合は failsafe-go を使用します。

この分離はアーキテクチャをクリーンに保ちます。小さなサービスクライアントが、失敗している依存リソースの呼び出しを停止するために、完全なレジレンスフレームワークを必要とするわけではありません。しかし、ゲートウェイ、アグリゲーター、APIクライアントSDK、または高トラフィックの統合レイヤーは、組み合わせたポリシーから恩恵を受けるかもしれません。

gobreakerのインストール

新しいコードにはv2パッケージを使用します:

go get github.com/sony/gobreaker/v2

次にインポートします:

import "github.com/sony/gobreaker/v2"

Go言語での基本的なサーキットブレーカー

以下はHTTP呼び出しの周りにある小さな例です。

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/sony/gobreaker/v2"
)

var ErrTemporaryUnavailable = errors.New("dependency temporarily unavailable")

type UserClient struct {
    baseURL string
    http    *http.Client
    cb      *gobreaker.CircuitBreaker[[]byte]
}

func NewUserClient(baseURL string) *UserClient {
    settings := gobreaker.Settings{
        Name:        "user-service",
        MaxRequests: 3,
        Interval:    30 * time.Second,
        Timeout:     10 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.ConsecutiveFailures >= 5
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            fmt.Printf("circuit breaker %s changed from %s to %s\n", name, from, to)
        },
    }

    return &UserClient{
        baseURL: baseURL,
        http: &http.Client{
            Timeout: 3 * time.Second,
        },
        cb: gobreaker.NewCircuitBreaker[[]byte](settings),
    }
}

func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
    result, err := c.cb.Execute(func() ([]byte, error) {
        req, err := http.NewRequestWithContext(
            ctx,
            http.MethodGet,
            c.baseURL+"/users/"+userID,
            nil,
        )
        if err != nil {
            return nil, err
        }

        resp, err := c.http.Do(req)
        if err != nil {
            return nil, err
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 {
            return nil, fmt.Errorf("user service returned %d", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("user not found")
        }

        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("user service client error: %d", resp.StatusCode)
        }

        return io.ReadAll(resp.Body)
    })

    if errors.Is(err, gobreaker.ErrOpenState) {
        return nil, ErrTemporaryUnavailable
    }

    if errors.Is(err, gobreaker.ErrTooManyRequests) {
        return nil, ErrTemporaryUnavailable
    }

    return result, err
}

これは完全な本番環境用のクライアントではありませんが、以下のような形を示しています:

  • ブレーカーが外部呼び出しをラップする
  • HTTPリクエストがコンテキストを受け取る
  • HTTPクライアントにタイムアウトがある
  • サーバー側の失敗がブレーカーの失敗としてカウントされる
  • オープンサーキットのエラーがアプリケーションエラーにマッピングされる

gobreaker設定の構成

主要な設定を理解する価値があります。

Name

Name はブレーカーを識別します。

安定した、具体的な名前を使用します:

payment-api
search-service
llm-gateway
user-service

以下のような曖昧な名前は避けてください:

http-client
external-call
default

この名前はログとメトリクスで必要になります。

MaxRequests

MaxRequests は、ブレーカーがハーフオープン状態の間、許可されるリクエストの数を制御します。

小さい数が通常より安全です。ハーフオープンの目的は、直ちにフルトラフィックを送るのではなく、回復をテストすることです。

Interval

Interval は、ブレーカーがクローズド状態の間、内部カウントがクリアされるタイミングを制御します。

ゼロの場合、カウントは自動的にクリアされません。ゼロ以外のインターバルは、ブレーカーにローリング的なメモリウィンドウを提供しますが、完全なスライディングウィンドウ実装と同じではありません。

Timeout

Timeout は、ブレーカーがハーフオープンに移動する前にオープン状態に留まる時間を制御します。

タイムアウトが短すぎると、回復していない依存リソースをサービスが継続的にプローブ(探査)し続けます。長すぎると、回復が遅延します。

10〜30秒といった保守的な値から始め、本番環境のメトリクスからチューニングします。

ReadyToTrip

ReadyToTrip は、ブレーカーがオープンすべきかどうかを決定します。

シンプルなルールは連続した失敗です:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 5
}

これは論理的ですが、高ボリュームサービスには適切でないかもしれません。

別のオプションは、最小リクエスト数後の失敗比率です:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    total := counts.Requests
    failures := counts.TotalFailures

    if total < 20 {
        return false
    }

    return float64(failures)/float64(total) >= 0.5
}

これにより、サンプルサイズが極めて小さい後のサーキットオープンを回避します。

OnStateChange

OnStateChange は、ログやメトリクスを発行すべき場所です。

最低限、以下を記録してください:

  • ブレーカー名
  • 旧状態
  • 新状態
  • タイムスタンプ

本番環境システムでは、ブレーカー状態をメトリクスとして公開します。ログはデバッグに有用ですが、メトリクスはアラートとダッシュボードには優れています。

IsSuccessful

IsSuccessful は、どのエラーが失敗としてカウントされるかを決定できます。

これは重要です。

すべてのエラーがブレーカーをオープンするべきではありません。例えば、ユーザーサービスからの 404 Not Found は有効なビジネス結果かもしれません。400 Bad Request は、依存リソースの過失ではなく、呼び出し側の過失かもしれません。

503 Service Unavailable、タイムアウト、接続リセット、または 429 Too Many Requests は、真の依存リソースの健康状態のシグナルかもしれません。

ここには注意が必要です。間違ったエラーをカウントすることは、ノイジーなサーキットブレーカーを構築する最も簡単な方法の一つです。

何を失敗とみなすべきか?

ここがエンジニアリングの判断が重要な場所です。

通常、以下を失敗としてカウントします:

  • ネットワークタイムアウト
  • 接続拒否
  • 接続リセット
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • 繰り返される429レスポンス
  • 依存リソースからの不正なレスポンス
  • 外部呼び出し中のコンテキストデッドライン超過

通常、以下を依存リソースの失敗としてカウントしません:

  • バリデーションエラー
  • ローカルシリアライゼーションエラー
  • 予期された404レスポンス
  • 呼び出し側の認証失敗
  • ビジネスルールの拒否
  • ユーザー入力エラー

ブレーカーは、一般的なアプリケーションの失敗ではなく、依存リソースの健康状態を表すべきです。

サーキットブレーカーと context.Context

Go言語において、サーキットブレーカーは context.Context の置き換えではありません。

サーキットブレーカーは呼び出しを試みるべきかどうかを決定します。コンテキストは、その呼び出しがどれほど実行され得るか、および呼び出し側がなくなった場合に停止すべきかどうかを制御します。

良い外部呼び出しには通常、両方が必要です:

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

data, err := client.GetUser(ctx, userID)

コンテキストは呼び出しチェーンを通じて伝播すべきです:

着信リクエストコンテキスト
-> サービスメソッド
-> クライアントメソッド
-> HTTPリクエスト
-> 依存リソース

リクエストスコープのコード内でデタッチされたバックグラウンドコンテキストを作成しないでください。ユーザーリクエストがキャンセルされた場合、下流の作業も通常停止すべきです。

静かなルールは以下の通りです:

ブレーカーはシステムを保護する。コンテキストはリクエストを保護する。

通常、両方が必要です。

サーキットブレーカーとリトライ

リトライとサーキットブレーカーは一緒に良く機能しますが、順序が重要です。

最も安全なデフォルトは以下の通りです:

試行ごとのタイムアウト
バックオフとジッターによるリトライ
依存リソース呼び出しの周りにサーキットブレーカー

しかし、普遍的な答えはありません。何をカウントしたいかについて考えます。

各リトライ試行がブレーカーを通過する場合、1つのユーザーリクエストが複数の失敗に寄与します。これによりブレーカーがより早くオープンする可能性がありますが、それは良いことか悪いことかです。

ブレーカーがリトライ操作全体をラップする場合、ブレーカーはユーザーリクエストごとに1つの最終的な成功または失敗を見ます。これは穏やかですが、失敗した試行の数を隠す可能性があります。

多くのアプリケーションサービスにとって、この形状は理にかなっています:

ユーザーリクエスト
-> サーキットブレーカー
   -> リトライポリシー
      -> タイムアウト付きの1つのHTTP試行

これは、ブレーカーが依存リソース操作が呼び出し側にとって最終的に機能したかどうかを追跡することを意味します。

より低レベルのクライアントでは、この形状も理にかないます:

ユーザーリクエスト
-> リトライポリシー
   -> サーキットブレーカー
      -> タイムアウト付きの1つのHTTP試行

これは、ブレーカーが各試行を保護することを意味します。

より重要なルールはこれです:

盲目的にリトライしない。

以下を使用します:

  • 小さな最大リトライカウント
  • 指数バックオフ
  • ジッター
  • 試行ごとのタイムアウト
  • 全体のリクエストデッドライン
  • 書き込みの冪等性
  • リトライ試行のメトリクス

それらなしでは、リトライは小さな障害をより大きなものに変える可能性があります。リトライの安全性に関する詳細な説明については、分散システムで実際に機能する冪等性 を参照してください。

サーキットブレーカーと冪等性

サーキットブレーカーはしばしばリトライの近くに現れ、リトライは冪等性の問題を提起します。

読み取り操作では、リトライは通常安全です。

書き込み操作では、リトライは危険になり得ます。

以下の決済呼び出しを検討してください:

POST /charge

リクエストがタイムアウトした場合、決済は失敗しましたか?おそらく。成功しましたがレスポンスが失われましたか?これもあり得ます。

冪等性キーなしでリトライすると、2回課金する可能性があります。

書き込み操作では、以下の一つ以上を使用します:

  • 冪等性キー
  • リクエストID
  • 操作ID
  • 一意制約
  • トランザクショナルアウトボックス
  • ワークフローオーケストレーション
  • 明示的な調整

サーキットブレーカーは、失敗している決済プロバイダーへの呼び出しを続けるのを止めることができますが、安全でないリトライを安全にすることはできません。

サーキットブレーカーとフォールバック

サーキットがオープンになると、サービスには計画が必要です。

可能なフォールバック戦略には以下が含まれます:

  • キャッシュされたデータを返す
  • 警告付きの古いデータを返す
  • 非クリティカルなセクションを省略する
  • 作業を後でキューイングする
  • 別のプロバイダーに切り替える
  • 一時的なエラーを返す
  • 劣化された機能性を表示する
  • リクエストを迅速に失敗させる

フォールバックは正直であるべきです。

例えば、以下は通常良いです:

{
  "status": "temporary_unavailable",
  "message": "Recommendations are temporarily unavailable"
}

以下はリスクがあります:

{
  "recommendations": []
}

空のリストは有効な結果のように見えるかもしれません。障害を隠し、ユーザーを混乱させ、デバッグを難しくする可能性があります。

サイレントなフォールバックは誘惑があります。それらはまた危険です。

サーキットブレーカーと観測可能性

観測可能性のないサーキットブレーカーは、主にサプライズジェネレーターです。

少なくともこれらのメトリクスを追跡します:

  • 現在のブレーカー状態
  • 状態変更
  • 許可された呼び出し
  • 拒否された呼び出し
  • 成功
  • 失敗
  • タイムアウト
  • フォールバックレスポンス
  • リトライ試行
  • 下流のレイテンシ
  • 下流のステータスコード

有用なラベルには以下が含まれます:

  • ブレーカー名
  • 依存リソース名
  • 操作名
  • ステータスクラス
  • エラーカテゴリ

ユーザーID、完全なURL、リクエストID、または生のエラーメッセージのような高基数ラベルは避けてください。

ダッシュボードからこれらの質問に答えるべきです:

  • 現在どのサーキットブレーカーがオープンですか?
  • どれほど頻繁にオープンしますか?
  • どの依存リソースがオープンを引き起こしましたか?
  • ユーザーはフォールバックレスポンスを見ていますか?
  • ブレーカーがオープンした後、レイテンシは改善しましたか?
  • ブレーカーがオープンする前に、リトライボリュームは急増しましたか?
  • 依存リソースは回復しましたか?

ブレーカーを観測できない場合、チューニングできません。メトリクスと相性の良い構造化ログについては、Goでのslogによる構造化ログ を参照してください。

より本番環境フレンドリーなHTTPクライアントの形状

実際のサービスでは、サーキットブレーカーロジックをハンドラーに散らばらせないでください。

依存リソースの周りに小さなクライアントパッケージを作成します。

例の構造:

internal/
  userservice/
    client.go
    errors.go
    metrics.go

ハンドラーはgobreakerの詳細を知るべきではありません。ドメインレベルのクライアントメソッドに依存すべきです:

type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

その後、実装には以下を含めることができます:

  • HTTPリクエストの作成
  • コンテキストの伝播
  • ブレーカーの実行
  • ステータスコードの処理
  • レスポンスのデコード
  • メトリクス
  • エラーのマッピング

これにより、レジレンスポリシーは依存リソースの境界の近くに保たれます。境界でのエラー分類の詳細については、Goエラーハンドリングアーキテクチャ:境界とパターン を参照してください。

アプリケーションアーキテクチャにおけるサーキットブレーカーの位置

サーキットブレーカーパターンは、統合境界に属します。

Goアプリケーションでは、通常以下を意味します:

graph LR A[Handler] --> B[Application Service] B --> C[Dependency Client] C --> D[Circuit Breaker] D --> E[HTTP / RPC / DB / Queue]

可能な限り、ビジネスロジックからブレーカーを遠ざけます。

ビジネスレイヤーは以下のドメインエラーを理解すべきです:

payment provider unavailable
recommendations unavailable
profile service timeout

gobreakerの状態を理解する必要はありません。

この分離はアーキテクチャをクリーンに保ちます:

  • トランスポートの懸念はクライアントに留まる
  • レジレンスポリシーは依存リソースの近くに留まる
  • ドメインロジックは読み取りやすいまま
  • ハンドラーは薄いまま
  • テストは書きやすくなる

この記事は 本番環境でのアプリアーキテクチャ トピックの一部であり、統合パターンにおける冪等性、アウトボックス、サガ、およびオーケストレーションガイドと並んでいます。

一般的な間違い

間違い1:タイムアウトなし

サーキットブレーカーは、呼び出しが返さない限り、遅い呼び出しを魔法のように停止しません。

外部操作が永遠にハングする可能性がある場合、ブレーカーは失敗を速やかに認識しないかもしれません。

常にタイムアウトを使用します。

間違い2:すべてに1つのグローバルブレーカー

すべての依存リソースに1つのブレーカーを使用しないでください。

失敗しているメールプロバイダーは、決済プロバイダーのサーキットを開くべきではありません。遅い検索エンドポイントは、ユーザープロフィール呼び出しをブロックすべきではありません。

失敗モードが異なる場合、個別の依存リソース操作に対して個別のブレーカーを使用します。

間違い3:呼び出し側エラーを依存リソースの失敗としてカウントする

サービスが不正な入力を送信し 400 Bad Request を受信した場合、それは通常、下流の障害ではありません。

自身のバグでブレーカーを訓練しないでください。

間違い4:冪等でない書き込みのリトライ

リトライは無料ではありません。書き込み、決済、メッセージ、または副作用を複製する可能性があります。

書き込みをリトライする前に冪等にする。

間違い5:フォールバックの後ろに障害を隠す

フォールバックはグレースフルに劣化すべきであり、現実を偽るべきではありません。

依存リソースがダウンしている場合、メトリクスとログはそのことを明確にするべきです。

間違い6:本番データなしでのチューニング

例からコピーされた閾値は、出発点に過ぎません。

以下に基づいてチューニングします:

  • リクエストボリューム
  • 通常のエラー率
  • 依存リソースのレイテンシ
  • ユーザーへの影響
  • 回復時間
  • フォールバックの品質

間違い7:キャパシティ管理の代わりにサーキットブレーカーを使用する

サーキットブレーカーは以下の代替ではありません:

  • ロードシェディング
  • レート制限
  • キュー制限
  • オートスケーリング
  • データベースチューニング
  • 接続プール制限
  • アップストリームクォータ

それはレジレンス戦略の一部です。

実用的なデフォルト

内部HTTP依存リソースを呼び出す典型的なGoサービスの場合、理にかなった出発点は以下のようになるかもしれません:

HTTPクライアントタイムアウト:2〜5秒
リクエストごとのコンテキストタイムアウト:呼び出し側のSLAに基づく
ブレーカー失敗ルール:5回の連続失敗、または20リクエスト後の50%の失敗率
オープンタイムアウト:10〜30秒
ハーフオープンリクエスト:1〜5
リトライカウント:1〜3試行
リトライバックオフ:ジッター付き指数バックオフ

これらは普遍的な値ではありません。それらは比較的 безопасな出発点です。

ユーザー向けAPIでは、総レイテンシ予算を厳密に保ちます。バックグラウンドジョブでは、より長い待機を許容するかもしれません。決済プロバイダーでは、リトライと冪等性にははるかに注意が必要です。

サーキットブレーカーチェックリスト

サーキットブレーカーを追加する前に、これらの質問に答えます:

  • 保護されている依存リソースは何ですか?
  • 保護されている操作は何ですか?
  • どのエラーが依存リソースの失敗としてカウントされますか?
  • どのエラーがブレーカーによって無視されるべきですか?
  • 各呼び出しに適用されるタイムアウトは何ですか?
  • リトライは許可されていますか?
  • 書き込みは冪等ですか?
  • サーキットがオープンになると何が起こりますか?
  • フォールバックはありますか?
  • フォールバックはメトリクスで可視ですか?
  • サーキットが継続的にオープンする場合、誰がアラートを受けますか?
  • デプロイ後、ブレーカーはどのようにチューニングされますか?

これらに答えられない場合、ブレーカーの追加はレジレンスよりも混乱を生む可能性があります。

Go言語でのサーキットブレーカーのテスト

内部状態マシンではなく、振る舞いをテストします。

有用なテストには以下が含まれます:

  • 依存リソースが成功し、レスポンスが返される
  • 依存リソースが反復的に失敗し、サーキットがオープンする
  • オープンサーキットが一時的なエラーを返す
  • クライアント側のバリデーションエラーがブレーカーをトリガーしない
  • コンテキストタイムアウトが尊重される
  • 期待どおりフォールバックレスポンスが返される
  • 状態変更時にメトリクスが発行される

統合スタイルのテストにはフェイクHTTPサーバーを使用します:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()

ユニットテストでは、依存リソースをインターフェースの後ろに隠し、フェイク実装を注入します。

テストを決定論的に保ちます。長い実時間のスリープを避けます。テストで短いブレーカータイムアウトを構成します。フェイク時間と孤立したバブルを使用した並行Goコードのテストの詳細については、testing/synctestによる並行Goコードのテスト を参照してください。

独自のサーキットブレーカーを構築すべきか?

小さなサーキットブレーカーを構築することは良い学習演習です。状態マシンを理解するのに役立ちます。

本番環境コードでは、ニーズが非常に特定のでない限り、メンテナンスされているライブラリを優先します。

本番環境のブレーカーは以下を処理する必要があります:

  • 並行性
  • 状態遷移
  • カウンター
  • ハーフオープンプローブ
  • コールバック
  • カスタム失敗分類
  • レースフリーな振る舞い
  • 予測可能なエラーハンドリング

それは不可能ではありませんが、微妙に間違えやすいです。

地味なライブラリが通常、より良い選択肢です。

結論

サーキットブレーカーパターンは、魔法の信頼性の粉ではありません。

Go言語では、小さな、明示的なレジレンススタックの一部として最も良く機能します:

コンテキストタイムアウト
+ バックオフとジッターによるリトライ
+ サーキットブレーカー
+ フォールバック
+ メトリクス

パターンは、特に遅くなったり部分的に利用不能になったりするリモートサービスの周りの依存リソース境界で最も有用です。

カスケード障害を停止するために使用します。依存リソースが明らかに不健康な場合に迅速に失敗するために使用します。過負荷のシステムに回復の余地を与えるために使用します。

しかし、タイムアウト、冪等性、観測可能性、またはクリーンアーキテクチャを無視する言い訳として使用しないでください。

良いサーキットブレーカーは、失敗をより明確でコストのかからないものにします。悪いものは、失敗をより神秘的にするだけです。

参考文献

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。