Skip to content

Circuit Breakerパターン

サーキットブレーカーパターンを詳しく解説します。

🎯 なぜCircuit Breakerが必要なのか

Section titled “🎯 なぜCircuit Breakerが必要なのか”

❌ 問題のある実装:

// ❌ 悪い例: 障害が伝播する
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;
}
}

問題点:

  1. 障害の伝播: 外部サービスの障害が自システムに影響する
  2. リソースの浪費: 失敗するリクエストを繰り返し送信
  3. パフォーマンスの低下: タイムアウトまで待機する必要がある
  4. 可用性の低下: 外部サービスの障害が自システムの可用性に影響

影響:

  • システム全体の可用性の低下
  • リソースの浪費
  • パフォーマンスの低下
  • ユーザー体験の悪化

改善された実装:

// ✅ 良い例: 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();
});
}
}

メリット:

  • 障害の分離: 外部サービスの障害が自システムに影響しない
  • リソースの節約: 失敗するリクエストを送信しない
  • パフォーマンスの向上: 即座に失敗を返す
  • 可用性の向上: フォールバック処理を実行できる

状態: 正常に動作している状態です。

動作:

// すべてのリクエストを通過させる
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;
}
}

状態: 障害が発生している状態です。

動作:

// すべてのリクエストを即座に失敗させる
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
throw new CircuitBreakerOpenError('Circuit breaker is 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;
}
}
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();
});
}
}
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',
};
}
}

設定例:

interface CircuitBreakerConfig {
// 失敗率の閾値(%)
failureRateThreshold: number; // 50%
// オープン状態の待機時間(ミリ秒)
waitDurationInOpenState: number; // 60000ms (60秒)
// スライディングウィンドウのサイズ
slidingWindowSize: number; // 10リクエスト
// 最低必要な呼び出し回数
minimumNumberOfCalls: number; // 5回
// ハーフオープン状態で許可する呼び出し回数
permittedNumberOfCallsInHalfOpenState: number; // 3回
// 自動的にハーフオープンに遷移するか
automaticTransitionFromOpenToHalfOpenEnabled: boolean; // true
}
// 決済サービス: 厳しい設定(可用性が重要)
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の実装により、障害に強いシステムを構築できます。