Circuit Breakerパターン
🔌 Circuit Breakerパターン
Section titled “🔌 Circuit Breakerパターン”サーキットブレーカーパターンを詳しく解説します。
🎯 なぜCircuit Breakerが必要なのか
Section titled “🎯 なぜCircuit Breakerが必要なのか”⚠️ 障害の伝播問題
Section titled “⚠️ 障害の伝播問題”❌ 問題のある実装:
// ❌ 悪い例: 障害が伝播するclass PaymentService { async chargePayment(orderId: string, amount: number): Promise<PaymentResult> { try { // 外部の決済サービスを呼び出し const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), });
if (!response.ok) { throw new Error('Payment failed'); }
return await response.json(); } catch (error) { // エラーが発生すると、呼び出し元にも影響する throw error; } }}
// 呼び出し元class OrderService { async createOrder(orderData: OrderData): Promise<Order> { const order = await db.order.create({ data: orderData });
// 決済サービスが障害を起こしている場合、ここでタイムアウト // → 注文は作成されているが、決済は完了していない // → データの不整合 await paymentService.chargePayment(order.id, orderData.amount);
return order; }}問題点:
- 障害の伝播: 外部サービスの障害が自システムに影響する
- リソースの浪費: 失敗するリクエストを繰り返し送信
- パフォーマンスの低下: タイムアウトまで待機する必要がある
- 可用性の低下: 外部サービスの障害が自システムの可用性に影響
影響:
- システム全体の可用性の低下
- リソースの浪費
- パフォーマンスの低下
- ユーザー体験の悪化
Circuit Breakerによる解決
Section titled “Circuit Breakerによる解決”改善された実装:
// ✅ 良い例: Circuit Breakerを使用class CircuitBreaker { private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'; private failureCount: number = 0; private lastFailureTime: Date | null = null; private readonly failureThreshold: number = 5; private readonly timeout: number = 60000; // 60秒 private readonly halfOpenTimeout: number = 30000; // 30秒
async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'OPEN') { // サーキットが開いている場合、即座に失敗を返す if (this.shouldAttemptReset()) { this.state = 'HALF_OPEN'; } else { throw new CircuitBreakerOpenError('Circuit breaker is open'); } }
try { const result = await fn();
// 成功した場合、失敗カウントをリセット this.onSuccess();
return result; } catch (error) { // 失敗した場合、失敗カウントを増やす this.onFailure(); throw error; } }
private onSuccess(): void { this.failureCount = 0;
if (this.state === 'HALF_OPEN') { // ハーフオープン状態で成功した場合、クローズ状態に戻す this.state = 'CLOSED'; } }
private onFailure(): void { this.failureCount++; this.lastFailureTime = new Date();
if (this.failureCount >= this.failureThreshold) { // 失敗閾値を超えた場合、サーキットを開く this.state = 'OPEN'; } }
private shouldAttemptReset(): boolean { if (!this.lastFailureTime) { return false; }
const elapsed = Date.now() - this.lastFailureTime.getTime(); return elapsed >= this.timeout; }}
class PaymentService { private circuitBreaker: CircuitBreaker;
constructor() { this.circuitBreaker = new CircuitBreaker(); }
async chargePayment(orderId: string, amount: number): Promise<PaymentResult> { return await this.circuitBreaker.execute(async () => { const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), timeout: 5000, // 5秒でタイムアウト });
if (!response.ok) { throw new Error('Payment failed'); }
return await response.json(); }); }}メリット:
- 障害の分離: 外部サービスの障害が自システムに影響しない
- リソースの節約: 失敗するリクエストを送信しない
- パフォーマンスの向上: 即座に失敗を返す
- 可用性の向上: フォールバック処理を実行できる
Circuit Breakerの状態
Section titled “Circuit Breakerの状態”1. CLOSED(閉じている)
Section titled “1. CLOSED(閉じている)”状態: 正常に動作している状態です。
動作:
// すべてのリクエストを通過させるif (this.state === 'CLOSED') { try { const result = await executeRequest(); return result; } catch (error) { this.failureCount++; if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; } throw error; }}2. OPEN(開いている)
Section titled “2. OPEN(開いている)”状態: 障害が発生している状態です。
動作:
// すべてのリクエストを即座に失敗させるif (this.state === 'OPEN') { if (this.shouldAttemptReset()) { this.state = 'HALF_OPEN'; } else { throw new CircuitBreakerOpenError('Circuit breaker is open'); }}3. HALF_OPEN(半開き)
Section titled “3. HALF_OPEN(半開き)”状態: 障害が回復したかテストしている状態です。
動作:
// 限られた数のリクエストを通過させるif (this.state === 'HALF_OPEN') { try { const result = await executeRequest(); this.state = 'CLOSED'; // 成功した場合、クローズ状態に戻す this.failureCount = 0; return result; } catch (error) { this.state = 'OPEN'; // 失敗した場合、オープン状態に戻す this.failureCount++; throw error; }}実践的な実装例
Section titled “実践的な実装例”Resilience4jを使用した実装
Section titled “Resilience4jを使用した実装”import { CircuitBreaker, CircuitBreakerConfig } from 'resilience4j';
// Circuit Breakerの設定const circuitBreakerConfig: CircuitBreakerConfig = { failureRateThreshold: 50, // 失敗率50%でオープン waitDurationInOpenState: 60000, // 60秒待機 slidingWindowSize: 10, // 10リクエストのスライディングウィンドウ minimumNumberOfCalls: 5, // 最低5回の呼び出しが必要 permittedNumberOfCallsInHalfOpenState: 3, // ハーフオープン状態で3回許可 automaticTransitionFromOpenToHalfOpenEnabled: true,};
const circuitBreaker = CircuitBreaker.of('payment-service', circuitBreakerConfig);
class PaymentService { async chargePayment(orderId: string, amount: number): Promise<PaymentResult> { return await circuitBreaker.executeSupplier(async () => { const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), });
if (!response.ok) { throw new Error('Payment failed'); }
return await response.json(); }); }}フォールバック処理
Section titled “フォールバック処理”class PaymentService { private circuitBreaker: CircuitBreaker;
async chargePayment(orderId: string, amount: number): Promise<PaymentResult> { try { return await this.circuitBreaker.execute(async () => { return await this.callPaymentAPI(orderId, amount); }); } catch (error) { if (error instanceof CircuitBreakerOpenError) { // サーキットが開いている場合、フォールバック処理を実行 return await this.fallbackPayment(orderId, amount); } throw error; } }
private async fallbackPayment(orderId: string, amount: number): Promise<PaymentResult> { // フォールバック処理: キューに保存して後で処理 await db.pendingPayments.create({ data: { orderId, amount, status: 'PENDING', createdAt: new Date(), }, });
return { status: 'PENDING', message: 'Payment will be processed later', }; }}Circuit Breakerの設定
Section titled “Circuit Breakerの設定”設定例:
interface CircuitBreakerConfig { // 失敗率の閾値(%) failureRateThreshold: number; // 50%
// オープン状態の待機時間(ミリ秒) waitDurationInOpenState: number; // 60000ms (60秒)
// スライディングウィンドウのサイズ slidingWindowSize: number; // 10リクエスト
// 最低必要な呼び出し回数 minimumNumberOfCalls: number; // 5回
// ハーフオープン状態で許可する呼び出し回数 permittedNumberOfCallsInHalfOpenState: number; // 3回
// 自動的にハーフオープンに遷移するか automaticTransitionFromOpenToHalfOpenEnabled: boolean; // true}サービスごとの設定
Section titled “サービスごとの設定”// 決済サービス: 厳しい設定(可用性が重要)const paymentCircuitBreakerConfig: CircuitBreakerConfig = { failureRateThreshold: 30, // 30%でオープン waitDurationInOpenState: 30000, // 30秒 slidingWindowSize: 20, minimumNumberOfCalls: 10,};
// 通知サービス: 緩い設定(可用性は重要だが、失敗しても問題ない)const notificationCircuitBreakerConfig: CircuitBreakerConfig = { failureRateThreshold: 70, // 70%でオープン waitDurationInOpenState: 120000, // 120秒 slidingWindowSize: 10, minimumNumberOfCalls: 5,};Circuit Breakerパターンのポイント:
- なぜ必要か: 障害の伝播を防ぎ、リソースを節約
- 状態: CLOSED(閉じている)、OPEN(開いている)、HALF_OPEN(半開き)
- 実装: Resilience4jなどのライブラリを使用
- フォールバック処理: サーキットが開いている場合の代替処理
- 設定: サービスごとに適切な設定値を選択
適切なCircuit Breakerの実装により、障害に強いシステムを構築できます。