Skip to content
Published on

サーキットブレーカーパターン完全ガイド — Resilience4jで実装するマイクロサービスの障害隔離

シェア
Authors
Circuit Breaker Pattern

はじめに

マイクロサービスアーキテクチャにおいて、サービス間の呼び出しは必然的です。しかし、あるサービスが遅延したり応答しなくなると、呼び出し側のサービスにも連鎖的に障害が伝播します。サーキットブレーカー(Circuit Breaker)パターンは、電気のブレーカーのように、障害が伝播する前に回路を遮断してシステム全体の安定性を守ります。

連鎖障害(Cascading Failure)の理解

正常状態:
[Client][API Gateway][Order Service][Payment Service][Bank API]
                                          [Inventory Service]

Payment Service障害時(サーキットブレーカーなし):
[Client] ← タイムアウト ← [API Gateway] ← タイムアウト ← [Order Service] ← タイムアウト ← [Payment Service]                         スレッド枯渇!              スレッド枯渇!

結果:Payment障害がシステム全体を停止させる

サーキットブレーカーの状態遷移

        失敗率が閾値未満                      失敗率が閾値以上
    ┌────────────────────┐              ┌────────────────────┐
    │                    │              │                    │
    ▼                    │              │                    ▼
┌────────┐         ┌────────────┐         ┌──────────┐
CLOSED │ ──────>HALF-OPEN<────── │   OPEN│(正常) │         │(試行許可)  │         │(遮断)    │
└────────┘         └────────────┘         └──────────┘
    ▲                    │                     │
    │                    │                     │
    └────────────────────┘                     │
        試行呼び出し成功                    待機時間経過
                                          (waitDurationInOpenState)
  • CLOSED: 正常状態、すべてのリクエストを許可
  • OPEN: 障害検知、すべてのリクエストを即座に失敗させる(フェイルファスト)
  • HALF-OPEN: 限定的な試行呼び出しを許可、成功すればCLOSEDに復帰

Resilience4j 実装

依存関係の追加

<!-- Maven -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>
// Gradle
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'

設定 (application.yml)

resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10 # 直近10リクエスト基準
        minimumNumberOfCalls: 5 # 最低5回呼び出し後に評価
        failureRateThreshold: 50 # 失敗率50%以上 → OPEN
        slowCallRateThreshold: 80 # 低速呼び出し80%以上 → OPEN
        slowCallDurationThreshold: 2s # 2秒以上 → 低速呼び出し
        waitDurationInOpenState: 30s # OPEN → HALF-OPEN待機時間
        permittedNumberOfCallsInHalfOpenState: 3 # HALF-OPENの試行呼び出し数
        automaticTransitionFromOpenToHalfOpenEnabled: true
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException
        ignoreExceptions:
          - com.example.BusinessException

  retry:
    instances:
      paymentService:
        maxAttempts: 3
        waitDuration: 1s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.io.IOException

  bulkhead:
    instances:
      paymentService:
        maxConcurrentCalls: 20
        maxWaitDuration: 500ms

  timelimiter:
    instances:
      paymentService:
        timeoutDuration: 3s
        cancelRunningFuture: true

サービス実装

@Service
@Slf4j
public class PaymentService {

    private final RestTemplate restTemplate;

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    @Retry(name = "paymentService")
    @Bulkhead(name = "paymentService")
    @TimeLimiter(name = "paymentService")
    public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
        log.info("Processing payment for order: {}", request.getOrderId());

        PaymentResponse response = restTemplate.postForObject(
            "http://payment-service/api/v1/payments",
            request,
            PaymentResponse.class
        );

        return CompletableFuture.completedFuture(response);
    }

    // フォールバックメソッド — サーキットOPEN時に呼び出される
    private CompletableFuture<PaymentResponse> paymentFallback(
            PaymentRequest request, Exception ex) {
        log.warn("Payment circuit breaker activated for order: {}. Reason: {}",
                request.getOrderId(), ex.getMessage());

        // 戦略1: キューに入れて後でリトライ
        paymentRetryQueue.add(request);

        // 戦略2: デフォルトレスポンスを返却
        return CompletableFuture.completedFuture(
            PaymentResponse.builder()
                .orderId(request.getOrderId())
                .status(PaymentStatus.PENDING)
                .message("Payment is being processed. You will receive confirmation shortly.")
                .build()
        );
    }
}

イベントモニタリング

@Component
public class CircuitBreakerEventListener {

    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerEventListeners() {
        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("paymentService");

        cb.getEventPublisher()
            .onStateTransition(event -> {
                log.warn("Circuit Breaker '{}' state transition: {} → {}",
                    event.getCircuitBreakerName(),
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState());

                // Slack/PagerDutyアラート
                if (event.getStateTransition().getToState() ==
                        CircuitBreaker.State.OPEN) {
                    alertService.sendAlert(
                        "CRITICAL: Payment circuit breaker OPEN!");
                }
            })
            .onError(event ->
                log.error("Circuit Breaker error: {} (duration: {}ms)",
                    event.getThrowable().getMessage(),
                    event.getElapsedDuration().toMillis())
            )
            .onSuccess(event ->
                log.debug("Circuit Breaker success (duration: {}ms)",
                    event.getElapsedDuration().toMillis())
            );
    }
}

Python実装 (pybreaker)

import pybreaker
import requests
from functools import wraps

# サーキットブレーカーの作成
payment_breaker = pybreaker.CircuitBreaker(
    fail_max=5,              # 5回失敗でOPEN
    reset_timeout=30,        # 30秒後にHALF-OPEN
    exclude=[ValueError],    # ビジネス例外を除外
)

# リスナーの登録
class CircuitBreakerListener(pybreaker.CircuitBreakerListener):
    def state_change(self, cb, old_state, new_state):
        print(f"Circuit '{cb.name}': {old_state.name}{new_state.name}")
        if new_state == pybreaker.STATE_OPEN:
            send_slack_alert(f"警告: {cb.name} circuit OPEN!")

    def failure(self, cb, exc):
        print(f"Circuit '{cb.name}' failure: {exc}")

payment_breaker.add_listener(CircuitBreakerListener())


# デコレーターとして使用
@payment_breaker
def process_payment(order_id: str, amount: float) -> dict:
    response = requests.post(
        "http://payment-service/api/v1/payments",
        json={"order_id": order_id, "amount": amount},
        timeout=3
    )
    response.raise_for_status()
    return response.json()


# フォールバック付きラッパー
def process_payment_safe(order_id: str, amount: float) -> dict:
    try:
        return process_payment(order_id, amount)
    except pybreaker.CircuitBreakerError:
        # サーキットOPEN状態 → 即座にフォールバック
        return {
            "order_id": order_id,
            "status": "PENDING",
            "message": "Payment queued for retry"
        }
    except requests.RequestException as e:
        # ネットワークエラー → サーキットに失敗が記録される
        return {
            "order_id": order_id,
            "status": "FAILED",
            "error": str(e)
        }

フォールバック戦略パターン

1. キャッシュフォールバック

private PaymentResponse paymentCacheFallback(PaymentRequest req, Exception ex) {
    // 最後に成功したレスポンスをキャッシュから返却
    return cache.getIfPresent("payment:" + req.getOrderId());
}

2. デフォルト値の返却

private List<Product> productFallback(String category, Exception ex) {
    // デフォルトのおすすめ商品を返却
    return defaultProducts.getByCategory(category);
}

3. 代替サービスの呼び出し

private PaymentResponse paymentBackupFallback(PaymentRequest req, Exception ex) {
    // バックアップ決済サービスを呼び出し
    return backupPaymentService.process(req);
}

4. キューイング後の非同期処理

private PaymentResponse paymentQueueFallback(PaymentRequest req, Exception ex) {
    // メッセージキューに入れて後で処理
    kafkaTemplate.send("payment-retry", req);
    return PaymentResponse.pending(req.getOrderId());
}

レジリエンスパターンの組み合わせ

// 適用順序(外側 → 内側):
// Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead

@Retry(name = "service")           // 3. 失敗時にリトライ
@CircuitBreaker(name = "service")  // 2. 失敗率のモニタリング
@Bulkhead(name = "service")        // 1. 同時実行数の制限
public Response callExternalService() {
    // ...
}
リクエストフロー:
[リクエスト]Bulkhead(同時20件制限)
CircuitBreaker(失敗率モニタリング)
Retry(失敗時最大3回リトライ)
TimeLimiter(3秒タイムアウト)
[外部サービス呼び出し]

Prometheusメトリクス

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    distribution:
      percentiles-histogram:
        resilience4j.circuitbreaker.calls: true
# サーキットブレーカーの状態
resilience4j_circuitbreaker_state{name="paymentService"}
# 0=CLOSED, 1=OPEN, 2=HALF_OPEN

# 失敗率
resilience4j_circuitbreaker_failure_rate{name="paymentService"}

# 呼び出し統計
rate(resilience4j_circuitbreaker_calls_total{name="paymentService",kind="successful"}[5m])
rate(resilience4j_circuitbreaker_calls_total{name="paymentService",kind="failed"}[5m])

# 低速呼び出し率
resilience4j_circuitbreaker_slow_call_rate{name="paymentService"}

テスト

@SpringBootTest
class CircuitBreakerTest {

    @Autowired
    private CircuitBreakerRegistry registry;

    @Test
    void shouldOpenCircuitAfterFailures() {
        CircuitBreaker cb = registry.circuitBreaker("paymentService");

        // CLOSED状態を確認
        assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.CLOSED);

        // 5回の失敗をシミュレーション
        for (int i = 0; i < 5; i++) {
            cb.onError(0, TimeUnit.MILLISECONDS, new IOException("timeout"));
        }

        // OPEN状態への遷移を確認
        assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN);

        // OPEN状態で即座に失敗
        assertThatThrownBy(() ->
            cb.decorateSupplier(() -> "test").get()
        ).isInstanceOf(CallNotPermittedException.class);
    }
}

クイズ

Q1. サーキットブレーカーの3つの状態は?

CLOSED(正常、全呼び出し許可)、OPEN(遮断、即座に失敗)、HALF-OPEN(限定的な試行呼び出しを許可)です。

Q2. failureRateThreshold: 50の意味は?

スライディングウィンドウ内で失敗率が50%以上になるとサーキットがOPEN状態に遷移します。

Q3. Resilience4jのアノテーション適用順序は?

外側から内側へ:Retry、CircuitBreaker、RateLimiter、TimeLimiter、Bulkheadの順に適用されます。

Q4. サーキットブレーカーなしでマイクロサービスに発生する問題は?

連鎖障害(Cascading Failure)です。1つの遅延サービスが呼び出し側サービスのスレッドを枯渇させ、システム全体が停止します。

Q5. HALF-OPEN状態で試行呼び出しがすべて成功すると?

サーキットがCLOSED状態に復帰し、通常通りすべての呼び出しを許可します。

Q6. Bulkheadパターンの役割は?

同時呼び出し数を制限し、1つのサービス呼び出しがすべてのスレッドを消費することを防ぎます。船の隔壁(Bulkhead)のように障害を隔離します。

Q7. フォールバック戦略4つを挙げてください。

キャッシュフォールバック(最後の成功レスポンス)、デフォルト値の返却、代替サービスの呼び出し、キューイング後の非同期処理の4つです。

まとめ

サーキットブレーカーパターンは、マイクロサービスアーキテクチャにおける必須の障害隔離メカニズムです。Resilience4jは軽量でモジュラーな実装を提供し、Retry、Bulkhead、TimeLimiterなどの他のレジリエンスパターンと組み合わせて堅牢な分散システムを構築できます。

参考資料