Go言語のCircuit Breaker実践ガイド:マイクロサービスの可用性を向上させる10の技法

分散システムにおいて、マイクロサービス間の依存関係は複雑であり、リモート呼び出しの失敗が連鎖的な障害(カスケード障害)を引き起こすリスクがあります。Go言語向けの軽量なフォールトトレランスライブラリである circuitbreaker を活用することで、サービス雪崩を防ぎ、システムの回復力を大幅に向上させることができます。本稿では、Goプロジェクトで circuitbreaker を効果的に適用するための10の実践的なアプローチを解説します。

1. 適切なブレーカータイプの選定

circuitbreaker は3つの主要なブレーカーを提供しています。ユースケースに合わせて最適なタイプを選択します。

  • 閾値ベース:累計失敗数が閾値に達すると開放(NewThresholdBreaker
  • 連続失敗ベース:連続失敗数が閾値に達すると開放(NewConsecutiveBreaker
  • エラーレートベース:スライディングウィンドウ内のエラー率が閾値を超えると開放(NewRateBreaker
// エラーレートブレーカーの生成:エラー率60%以上、かつ最低200リクエストでトリガー
breaker := circuit.NewRateBreaker(0.6, 200)

2. タイムアウトの適切な設定とリソース保護

リモート呼び出しには必ずタイムアウトを設定し、リソースの枯渇を防ぎます。Call メソッドの第2引数でタイムアウトを指定し、時間超過を失敗としてカウントさせます。

// 3秒のタイムアウトを指定して実行
err := breaker.Call(func() error {
    _, reqErr := http.Get("https://api.internal.service/users")
    return reqErr
}, 3*time.Second)

タイムアウト値は、対象サービスのSLAに基づき、通常は95パーセンタイルのレスポンス時間にバッファを加えた値に設定します。

3. 状態遷移のイベント監視とアラート連携

Subscribe() を利用してブレーカーの状態変化を捕捉し、ロギングやアラートシステムと連携させます。

stateChanges := breaker.Subscribe()
go func() {
    for event := range stateChanges {
        switch event {
        case circuit.BreakerTripped:
            log.Warn("サーキットブレーカーが開放状態になりました")
        case circuit.BreakerReset:
            log.Info("サーキットブレーカーが閉鎖状態に復旧しました")
        }
    }
}()

主要なイベントタイプには、BreakerTripped(開放)、BreakerReset(閉鎖)、BreakerFail(失敗)、BreakerReady(半開放/就绪)があります。

4. 手動制御による柔軟なフォールバック

自動判定だけでは要件を満たせない場合、Ready()Fail()Success() を用いて手動で状態を制御します。

if breaker.Ready() {
    result, execErr := executeComplexTask()
    if execErr != nil || result.IsInvalid() {
        breaker.Fail() // 失敗としてカウント
    } else {
        breaker.Success() // 成功としてカウント
    }
} else {
    // フォールバック処理を実行
}

このアプローチは、ビジネスロジックに基づく複雑な成功/失敗の判定が必要な場面で有効です。

5. HTTPクライアントのラップによる簡潔な実装

標準のHTTPクライアントにブレーカー機能を付与するには、専用クライアントを利用するとボイラープレートを削減できます。

// タイムアウト4秒、連続5回失敗で開放するHTTPクライアント
httpClient := circuit.NewHTTPClient(4*time.Second, 5, nil)
response, fetchErr := httpClient.Get("https://api.external.com/v1/resources")

内部でタイムアウトと失敗カウントが自動的に処理されるため、追加の実装は不要です。

6. カスタムトリガー条件によるビジネスロジックの反映

ShouldTrip をカスタマイズし、特定のHTTPステータスコードやビジネスエラーコードのみを失敗としてカウントするように設定できます。

customBreaker := circuit.NewBreakerWithOptions(&circuit.Options{
    ShouldTrip: func(b *circuit.Breaker) bool {
        // 503 Service Unavailable が連続4回発生した場合のみ開放
        return b.ConsecFailures() >= 4 && lastStatusCode == http.StatusServiceUnavailable
    },
})

7. バックオフ戦略による回復時のトラフィック制御

デフォルトの指数バックオフ戦略を調整し、障害復旧直後のトラフィック集中を緩和します。

expBackoff := backoff.NewExponentialBackOff()
expBackoff.InitialInterval = 2 * time.Second  // 初期インターバル
expBackoff.MaxInterval = 60 * time.Second     // 最大インターバル

resilientBreaker := circuit.NewBreakerWithOptions(&circuit.Options{
    BackOff: expBackoff,
})

8. スライディングウィンドウのチューニング

エラーレート計算の精度を高めるため、ウィンドウサイズとバケット数を調整します。

precisionBreaker := circuit.NewBreakerWithOptions(&circuit.Options{
    WindowTime:    60 * time.Second, // 60秒のウィンドウ
    WindowBuckets: 12,               // 5秒ごとのバケット
    ShouldTrip:    circuit.RateTripFunc(0.8, 100), // エラー率80%かつ100サンプル
})

9. 並行処理におけるスレッドセーフティの確保

内部でアトミック操作とミューテックスを使用しているため、複数のゴルーチンから同時に呼び出しても安全です。

var waitGroup sync.WaitGroup
for idx := 0; idx < 50; idx++ {
    waitGroup.Add(1)
    go func(id int) {
        defer waitGroup.Done()
        _ = breaker.Call(func() error {
            return processRequest(id)
        }, 2*time.Second)
    }(idx)
}
waitGroup.Wait()

10. 仮想時間を用いたテスト戦略

状態遷移のテストには、モッククロックを利用して時間の経過をシミュレートします。

fakeClock := clock.NewMock()
testBreaker := circuit.NewBreakerWithOptions(&circuit.Options{
    Clock: fakeClock,
})

// 失敗をシミュレート
for i := 0; i < 15; i++ {
    testBreaker.Fail()
}

if !testBreaker.Tripped() {
    t.Fatal("期待通りにブレーカーが開放されませんでした")
}

// 時間を進めてリセットを促す
fakeClock.Add(15 * time.Second)

if !testBreaker.Ready() {
    t.Fatal("時間経過後もブレーカーが就绪状態になりませんでした")
}

タグ: Go circuitbreaker Microservices fault-tolerance distributed-systems

8月12日 20:22 投稿