フレームワークレベルでのサーキットブレーカーによる回復力のあるシステムの構築
Wenhao Wang
Dev Intern · Leapcell

はじめに
現代の複雑な分散システムの世界では、単一障害点がすぐに広範囲な障害へとエスカレートする可能性があります。サービスは絶えず通信しており、1つのコンポーネントが利用できなくなったり応答が遅くなったりすると、上流のサービスに不釣り合いな影響を与え、カスケード障害として知られるドミノ効果を引き起こす可能性があります。在庫サービスが応答しなくなったeコマースプラットフォームを想像してみてください。注文処理サービスが在庫への失敗したリクエストを再試行し続けると、それ自身のリソースが枯渇し、遅くなったり利用できなくなったりする可能性があります。これは、ユーザー向けのストアフロントに影響を与え、システム全体の崩壊につながる可能性があります。このようなシナリオを防ぐことは、システムの安定性を維持し、良好なユーザーエクスペリエンスを確保するために最も重要です。この記事では、バックエンドフレームワーク内にサーキットブレーカーパターンを直接実装することにより、これらのリスクを積極的に軽減し、障害を隔離して拡散を防ぐ方法について詳しく説明します。
コアコンセプトの理解
実装の詳細に入る前に、関連する主要な用語を共通理解しましょう。
- 分散システム: コンポーネントがネットワーク化された異なるコンピューターに配置され、互いにメッセージをやり取りすることで通信および調整を行うシステム。
- カスケード障害: システム内の障害が successive stages を経て広がり、その影響を伝播させ、相互接続されたシステム全体をダウンさせる可能性がある障害。
- 回復力: システムが障害から回復し、完全に失敗するのではなく、場合によっては縮小した容量で機能し続ける能力。
- サーキットブレーカーパターン: アプリケーションが失敗する可能性が高い操作の実行を繰り返し試みるのを防ぐように設計されたアーキテクチャパターン。失敗する可能性のある関数呼び出しをラップし、障害を監視します。障害が一定のしきい値に達すると、サーキットブレーカーがトリップし、ラップされた関数への後続のすべての呼び出しは、試行することなくすぐにエラーを返します。これにより、失敗したサービスが回復するための時間が与えられ、呼び出しサービスが不運な呼び出しのリソースを浪費するのを防ぎます。
サーキットブレーカーパターンは3つの状態を操作します。
- クローズド: この状態では、サーキットブレーカーはリクエストを保護された操作に通過させます。障害が発生した場合、サーキットブレーカーはそれを記録します。一定期間内に失敗の数が定義済みのしきい値を超えると、サーキットブレーカーは オープン 状態にトリップします。
- オープン: この状態では、サーキットブレーカーは保護された操作を呼び出すことなく、すべてのリクエストを即座に失敗させます。設定されたタイムアウト後、 ハーフオープン 状態に遷移します。
- ハーフオープン: この状態では、サーキットブレーカーは保護された操作に少数のテストリクエストを通過させます。これらのテストリクエストが成功すると、サーキットブレーカーは クローズド 状態にリセットされます。失敗すると、すぐにタイムアウト期間のため オープン 状態に戻ります。
フレームワークレベルでのサーキットブレーカーの実装
フレームワークレベルでサーキットブレーカーを実装すると、大きな利点があります。障害対策ロジックを一元化し、個々のサービスでの定型コードを削減し、システム全体でパターンの適用を一貫させます。ここでは、Goで書かれた架空のマイクロサービスアーキテクチャと Hystrix ライブラリを使用します(ただし、原則はJavaの Resilience4j やPythonの Tenacity など、他の言語やフレームワークにも広く適用できます)。
私たちの Order Service が Payment Service を呼び出す必要があるシナリオを考えてみましょう。Payment Service の障害から Order Service を保護したいと考えています。
まず、Payment Service クライアントを定義しましょう。
// payment_client.go package main import ( "errors" "fmt" time "time" ) // PaymentServiceClient は外部決済サービスへの呼び出しをシミュレートします type PaymentServiceClient interface { ProcessPayment(orderID string, amount float64) error } type mockPaymentServiceClient struct { failRequests bool failRate int // 失敗させるリクエストの割合 latency time.Duration callCount int } func NewMockPaymentServiceClient(failRequests bool, failRate int, latency time.Duration) *mockPaymentServiceClient { return &mockPaymentServiceClient{ failRequests: failRequests, failRate: failRate, latency: latency, } } func (m *mockPaymentServiceClient) ProcessPayment(orderID string, amount float64) error { m.callCount++ time.Sleep(m.latency) if m.failRequests && m.callCount%100 < m.failRate { fmt.Printf("PaymentServiceClient: Simulating failure for order %s\n", orderID) return errors.New("payment service unavailable or timed out") } if m.callCount%10 == 0 { // ハーフオープン状態のテストのために、障害中でも時折成功をシミュレートします fmt.Printf("PaymentServiceClient: Payment processed successfully for order %s\n", orderID) } else { fmt.Printf("PaymentServiceClient: Payment processed successfully for order %s\n", orderID) } return nil }
次に、Hystrix をフレームワークレベル、おそらくカスタムHTTPクライアントまたはサービスラッパーに統合しましょう。
// main.go package main import ( "fmt" "log" time "time" "github.com/afex/hystrix-go/hystrix" ) // PaymentServiceCircuitBreakerClient は実際の決済サービスクライアントをHystrixでラップします type PaymentServiceCircuitBreakerClient struct { paymentClient PaymentServiceClient commandName string } func NewPaymentServiceCircuitBreakerClient(client PaymentServiceClient, commandName string) *PaymentServiceCircuitBreakerClient { // この特定のコマンドのHystrixを設定します hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{ Timeout: 1000, // コマンド実行のタイムアウト(ミリ秒) MaxConcurrentRequests: 10, // 許可される最大同時リクエスト数 RequestVolumeThreshold: 5, // サーキットをトリップするために必要な、ローリング統計ウィンドウ内の最小リクエスト数 ErrorPercentThreshold: 50, // サーキットをトリップするために必要な障害の割合 SleepWindow: 5000, // サーキットが開いた後、Hystrixが1つのリクエストを通過させるのを許可する時間(ミリ秒) }) return &PaymentServiceCircuitBreakerClient{ paymentClient: client, commandName: commandName, } } func (c *PaymentServiceCircuitBreakerClient) ProcessPayment(orderID string, amount float64) error { var err error err = hystrix.Do(c.commandName, func() error { // これは決済サービスへの実際の呼び出しです return c.paymentClient.ProcessPayment(orderID, amount) }, func(e error) error { // これはフォールバック関数です。コマンドが失敗した場合やサーキットが開いている場合に実行されます。 log.Printf("Fallback triggered for order %s due to error: %v", orderID, e) // ここでエラーをログに記録したり、再試行のために支払いをキューに入れたり、デフォルトの応答を返したりすることができます。 return fmt.Errorf("payment processing fallback triggered for order %s: %w", orderID, e) }) return err } func main() { fmt.Println("Starting Payment Service Circuit Breaker Demo") // 決済サービスの障害と遅延をシミュレートします // 最初は、頻繁に失敗するようにします mockClient := NewMockPaymentServiceClient(true, 70, 50*time.Millisecond) // クライアントをサーキットブレーカーでラップします cbClient := NewPaymentServiceCircuitBreakerClient(mockClient, "payment_service_process_payment") fmt.Println("\n--- Phase 1: High Failure Rate ---") // サーキットをトリップするために多くのリクエストをシミュレートします for i := 0; i < 20; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Error processing payment for %s: %v\n", orderID, err) } else { fmt.Printf("Successfully processed payment for %s\n", orderID) } time.Sleep(100 * time.Millisecond) // リクエスト間のわずかな遅延をシミュレートします } fmt.Println("\n--- Circuit Breaker Status ---") // しばらくすると、サーキットは開いているはずです。 // Hystrixダッシュボードまたはメトリクスは、実際のシステムではこれを示すでしょう。 // このデモでは、フォールバックメッセージで観察します。 time.Sleep(2 * time.Second) // サーキットが開くまで時間がかかるようにします fmt.Println("\n--- Phase 2: Circuit Open - Requests are immediately rejected ---") for i := 20; i < 30; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Error processing payment for %s: %v\n", orderID, err) } else { fmt.Printf("Successfully processed payment for %s\n", orderID) } time.Sleep(50 * time.Millisecond) } fmt.Println("\n--- Phase 3: Waiting for SleepWindow to allow Half-Open ---") fmt.Println("Simulating recovery of Payment Service. Reducing failure rate.") // 決済サービスが回復するのをシミュレートします mockClient.failRequests = false // 障害なし mockClient.failRate = 0 time.Sleep(6 * time.Second) // HystrixのSleepWindow(5秒)を過ぎるのを待ちます fmt.Println("\n--- Phase 4: Half-Open State - Test requests sent, circuit should close ---") for i := 30; i < 40; i++ { orderID := fmt.Sprintf("order-%d", i) err := cbClient.ProcessPayment(orderID, 100.0) if err != nil { fmt.Printf("Error processing payment for %s: %v\n", orderID, err) } else { fmt.Printf("Successfully processed payment for %s\n", orderID) } time.Sleep(100 * time.Millisecond) } fmt.Println("\nDemo Finished.") }
この例では、次のことを行います。
PaymentServiceClientインターフェースと、ネットワーク呼び出しや障害をシミュレートするmockPaymentServiceClientを定義します。PaymentServiceCircuitBreakerClientは、フレームワークレベルのラッパーとして機能します。実際のPaymentServiceClientインスタンスとcommandNameを受け取ります。hystrix.ConfigureCommandは、特定のコマンド名のサーキットブレーカーのしきい値を設定します。この設定は一度だけ、通常はアプリケーションの起動時またはサービス初期化時に行われます。ProcessPaymentメソッドは、hystrix.Doを使用して実際の決済処理ロジックを実行します。また、プライマリコマンドが失敗した場合やサーキットが開いている場合に呼び出されるfallback関数も提供します。フォールバックは、呼び出しサービスがブロックされたり即座に失敗したりするのを防ぎます。
出力は明確に示されます。
- 初期の失敗がサーキットを開かせます。
- サーキットが開いているときに、リクエストはフォールバックエラーですぐに拒否されます。
SleepWindowの後、少数のテストリクエストが通過できる(ハーフオープン)可能性があり、成功するとサーキットは閉じられます。
アプリケーションシナリオ:
- 外部API呼び出し: 信頼性の低いサードパーティAPIからサービスを保護します。
- データベースアクセス: クエリの遅延や接続の問題が発生した場合にデータベースの過負荷を防ぎます。
- サービス間通信: ダウンストリームマイクロサービスの障害からアップストリームサービスを保護します。
- キャッシングレイヤー: キャッシュサービスが利用できなくなった場合、サーキットブレーカーは回復するまで直接データベースヒットを防ぎ、適切であれば古いデータまたはフォールバックを使用できます。
結論
フレームワークレベルでサーキットブレーカーパターンを実装することは、回復力のあるバックエンドシステムを構築するための強力な戦略です。障害処理をカプセル化し、障害対策への一貫したアプローチを提供し、最も重要なのは、軽微な問題が壊滅的なカスケード障害にエスカレートするのを防ぎます。障害を隔離し、即時のフィードバックまたはフォールバックメカニズムを提供することにより、サーキットブレーカーは、アプリケーションがクラッシュするのではなく、正常に低下することを可能にし、不利な条件下での安定性と信頼性を大幅に向上させます。このパターンを採用して、機能するだけでなく、真に持続するシステムを設計してください。

