分散システムにおいて、マイクロサービス間の依存関係は複雑であり、リモート呼び出しの失敗が連鎖的な障害(カスケード障害)を引き起こすリスクがあります。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("時間経過後もブレーカーが就绪状態になりませんでした")
}