Skip to content
Published on

サーキットブレーカーとレジリエンスパターン実践ガイド — Resilience4j、Istio、障害分離戦略

Authors

はじめに

マイクロサービスアーキテクチャにおいて、サービス間呼び出しは本質的に不安定だ。ネットワーク遅延、タイムアウト、ダウンストリームサービスの障害は日常的に発生し、適切な防御メカニズムがなければ、単一サービスの障害がシステム全体に伝播する**連鎖障害(Cascading Failure)**を引き起こす。2024年のブラックフライデー期間中、大手ECプラットフォームの商品レコメンドサービスの応答遅延が商品一覧ページ全体を20秒以上のローディングにしてしまった事例が代表的だ。

こうした問題を解決するために登場したのがレジリエンスパターン(Resilience Patterns)だ。Michael Nygardが2007年の著書 Release It! で初めて紹介したサーキットブレーカーパターンを皮切りに、Bulkhead、Retry、Rate Limiter、Timeout、Fallbackなど様々なパターンが体系化された。Netflix Hystrixが最初の大衆的な実装だったが、2018年にメンテナンスモードに入って以降、Resilience4jがJava/Springエコシステムの事実上の標準となり、サービスメッシュ環境ではIstioがインフラレベルのサーキットブレーカーを提供している。

本記事では、サーキットブレーカーの動作原理からResilience4jとIstioを活用した実践的な実装、複合レジリエンスパターンの設計、Hystrixマイグレーション、運用モニタリング、障害事例分析まで包括的に扱う。


1. サーキットブレーカーパターンの原理

サーキットブレーカーは電気回路の遮断器に着想を得たパターンで、リモートサービス呼び出しの失敗を検知し、自動的に呼び出しを遮断してシステム全体の連鎖障害を防止する。

1.1 3つの状態:Closed、Open、Half-Open

              失敗率 >= 閾値 (failureRateThreshold)
    +---------------------------------------------+
    |                                             |
    v                                             |
+----------+                                 +----------+
|          |     試行呼び出し成功率 >= 閾値     |          |
|   OPEN   | <- - - - - - - - - - - - - - -- |  CLOSED  |
|  (遮断)   |                                 |  (正常)   |
+----------+                                 +----------+
     |                                            ^
     | waitDurationInOpenState 経過                |
     v                                            |
+--------------+    試行呼び出し成功               |
|  HALF-OPEN   | ---------------------------------+
|  (試行許可)  |
+--------------+
     |
     | 試行呼び出し失敗
     v
+----------+
|   OPEN   |  (再び遮断)
+----------+

各状態の動作は以下の通り。

状態動作遷移条件
CLOSEDすべてのリクエストを正常に通過させ、結果をスライディングウィンドウに記録失敗率が閾値以上でOPENに遷移
OPENすべてのリクエストを即座に拒否し、CallNotPermittedException発生waitDuration経過後にHALF-OPENに遷移
HALF-OPEN制限された数の試行呼び出しのみ許可試行呼び出し成功率に応じてCLOSEDまたはOPENに遷移

1.2 スライディングウィンドウ方式

Resilience4jは2種類のスライディングウィンドウをサポートする。

  • COUNT_BASED:直近N個の呼び出し結果を基準に失敗率を計算。トラフィックが一定のサービスに適している。
  • TIME_BASED:直近N秒以内の呼び出し結果を基準に失敗率を計算。トラフィック変動が大きいサービスに適している。

2. Resilience4jを活用したJava/Springサーキットブレーカー実装

2.1 依存関係の設定

// build.gradle (Spring Boot 3.x)
dependencies {
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
    implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

2.2 application.yml設定

resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 80
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException
        ignoreExceptions:
          - com.example.BusinessException

  retry:
    instances:
      paymentService:
        maxAttempts: 3
        waitDuration: 500ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException

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

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

  ratelimiter:
    instances:
      paymentService:
        limitRefreshPeriod: 1s
        limitForPeriod: 50
        timeoutDuration: 0s

2.3 アノテーションベースの実装

@Service
@Slf4j
public class PaymentService {

    private final PaymentGatewayClient paymentGatewayClient;
    private final PaymentCacheService paymentCacheService;

    public PaymentService(PaymentGatewayClient paymentGatewayClient,
                          PaymentCacheService paymentCacheService) {
        this.paymentGatewayClient = paymentGatewayClient;
        this.paymentCacheService = paymentCacheService;
    }

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    @Bulkhead(name = "paymentService")
    @Retry(name = "paymentService")
    @TimeLimiter(name = "paymentService")
    public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            log.info("決済リクエスト処理中: orderId={}", request.getOrderId());
            return paymentGatewayClient.charge(request);
        });
    }

    // フォールバックメソッド:サーキットがOPENまたは例外発生時に呼び出し
    private CompletableFuture<PaymentResponse> paymentFallback(
            PaymentRequest request, Throwable throwable) {
        log.warn("決済サービスフォールバック実行: orderId={}, reason={}",
                request.getOrderId(), throwable.getMessage());

        if (throwable instanceof CallNotPermittedException) {
            // サーキットが開いた状態 - キューに保存して非同期処理
            return CompletableFuture.completedFuture(
                PaymentResponse.queued(request.getOrderId(),
                    "決済サービス一時障害。注文がキューに登録されました。")
            );
        }

        // その他の例外 - キャッシュされた結果を返却試行
        return CompletableFuture.completedFuture(
            paymentCacheService.getCachedResponse(request.getOrderId())
                .orElse(PaymentResponse.error(request.getOrderId(),
                    "決済処理中にエラーが発生しました。しばらく後に再度お試しください。"))
        );
    }
}

Aspect実行順序:Resilience4jのアノテーションは以下の順序でネスト適用される。

Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )

最も外側のRetryが最後に適用されるため、CircuitBreakerが例外をスローするとRetryがリトライを実行する。この順序は各モジュールの*AspectOrderプロパティでカスタマイズ可能。


3. Istioサービスメッシュレベルのサーキットブレーカー

Istioはアプリケーションコードの変更なしにインフラレベルでサーキットブレーカーを適用できる。EnvoyプロキシのOutlier Detection機能を活用して、異常なインスタンスをロードバランシングプールから自動除去する。

3.1 DestinationRule設定

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment-service-circuit-breaker
  namespace: production
spec:
  host: payment-service.production.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100 # 最大TCP接続数
        connectTimeout: 3s # TCP接続タイムアウト
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 50 # 待機中のHTTPリクエスト最大数
        http2MaxRequests: 100 # アクティブHTTP/2リクエスト最大数
        maxRequestsPerConnection: 10 # 接続あたり最大リクエスト数
        maxRetries: 3 # 最大リトライ回数
    outlierDetection:
      consecutive5xxErrors: 5 # 連続5xxエラー5回で除去
      interval: 10s # 分析間隔
      baseEjectionTime: 30s # 最小除去時間
      maxEjectionPercent: 50 # 最大除去比率(50%)
      minHealthPercent: 30 # 最小正常インスタンス比率

3.2 Istio vs アプリケーションレベルのサーキットブレーカー

区分Istio(インフラレベル)Resilience4j(アプリレベル)
適用方式コード変更不要、YAML設定アノテーションまたはプログラマティックAPI
分離単位インスタンス(Pod)単位の除去メソッド/サービス単位の遮断
フォールバック未サポート(503を返却)カスタムフォールバックメソッドサポート
言語非依存すべての言語/フレームワーク対応Java/Kotlin専用
細かい制御限定的非常に細やか
モニタリングKiali、Grafana連携Micrometer、Actuator連携
推奨事例多言語環境、基本的な保護ビジネスロジック連動が必要な場合

実務では両方を併用することが推奨される。Istioがインフラレベルで異常インスタンスを隔離し、Resilience4jがアプリケーションレベルで細やかなフォールバックとリトライを処理する構造だ。


4. Bulkheadパターン:障害分離戦略

Bulkhead(隔壁)パターンは船舶の隔壁に由来する概念で、一つの区画が浸水しても他の区画は影響を受けないように隔離する戦略だ。

4.1 Semaphore Bulkhead vs ThreadPool Bulkhead

区分Semaphore BulkheadThreadPool Bulkhead
分離方式セマフォで同時呼び出し数を制限別スレッドプールで実行
呼び出しスレッドリクエストスレッドで直接実行別スレッドで非同期実行
戻り値型同期/非同期どちらもサポートCompletableFutureのみサポート
オーバーヘッド低いスレッドプール管理コストあり
推奨事例一般的な同時実行制限完全なスレッド分離が必要な場合
# ThreadPool Bulkhead設定
resilience4j:
  thread-pool-bulkhead:
    instances:
      inventoryService:
        maxThreadPoolSize: 10
        coreThreadPoolSize: 5
        queueCapacity: 20
        keepAliveDuration: 100ms
        writableStackTraceEnabled: true

4.2 サービス別Bulkhead分離の例

@Service
public class OrderOrchestrator {

    @Bulkhead(name = "paymentService", type = Bulkhead.Type.SEMAPHORE)
    public PaymentResult processPayment(Order order) {
        return paymentClient.charge(order.getPaymentInfo());
    }

    @Bulkhead(name = "inventoryService", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<InventoryResult> reserveInventory(Order order) {
        return CompletableFuture.supplyAsync(() ->
            inventoryClient.reserve(order.getItems()));
    }

    @Bulkhead(name = "notificationService", type = Bulkhead.Type.SEMAPHORE)
    public void sendNotification(Order order) {
        notificationClient.send(order.getUserId(), "ご注文を承りました。");
    }
}

このようにサービス別にBulkheadを分離すると、在庫サービスが遅くなっても決済サービスの同時呼び出し容量は影響を受けない。


5. Retry + Timeout + Rate Limiter組み合わせパターン

レジリエンスパターンは単独で使用するよりも組み合わせて使用する場合に最も効果的だ。ただし、誤った組み合わせはかえって障害を悪化させる可能性があるため注意が必要だ。

5.1 パターン組み合わせ時の注意点

  • Retry + CircuitBreaker:Retry単独使用は障害サービスに負荷をかける。必ずCircuitBreakerと併用し、一定の失敗率以上でリトライ自体を遮断すべきだ。
  • Timeout + Retry:合計所要時間 = timeout * maxAttempts。タイムアウト3秒でリトライ3回なら、最悪の場合9秒かかる。ユーザー応答時間のSLAを考慮して設計すべきだ。
  • Rate Limiter + CircuitBreaker:外部APIの呼び出し制限(Rate Limit)を超えないようRate Limiterを適用し、API自体の障害にはCircuitBreakerが対応する二重防御構造だ。

5.2 プログラマティックAPIを活用した組み合わせ

@Configuration
public class ResilienceConfig {

    @Bean
    public Supplier<String> resilientSupplier(
            CircuitBreakerRegistry circuitBreakerRegistry,
            RetryRegistry retryRegistry,
            BulkheadRegistry bulkheadRegistry,
            RateLimiterRegistry rateLimiterRegistry) {

        CircuitBreaker circuitBreaker = circuitBreakerRegistry
                .circuitBreaker("externalApi");
        Retry retry = retryRegistry.retry("externalApi");
        Bulkhead bulkhead = bulkheadRegistry.bulkhead("externalApi");
        RateLimiter rateLimiter = rateLimiterRegistry
                .rateLimiter("externalApi");

        // デコレーターチェーニング:内側から外側に適用
        Supplier<String> decoratedSupplier = Decorators
                .ofSupplier(() -> externalApiClient.call())
                .withBulkhead(bulkhead)           // 1. 同時呼び出し制限
                .withRateLimiter(rateLimiter)      // 2. レート制限
                .withCircuitBreaker(circuitBreaker) // 3. 失敗検知/遮断
                .withRetry(retry)                   // 4. リトライ
                .withFallback(Arrays.asList(
                    CallNotPermittedException.class,
                    BulkheadFullException.class,
                    RequestNotPermitted.class),
                    throwable -> "Fallback Response")
                .decorate();

        return decoratedSupplier;
    }
}

6. フォールバック戦略の設計

フォールバックは元のサービスが失敗した際に提供する代替応答だ。単にエラーメッセージを返すのではなく、ユーザー体験を最大限維持しながらgraceful degradationを実装することが核心だ。

6.1 フォールバック戦略の類型

戦略説明適用例
キャッシュフォールバック最後の成功レスポンスをキャッシュして返却商品レコメンド、為替情報、天気データ
デフォルト値フォールバック事前定義のデフォルト値を返却設定サービス、機能フラグ
キューフォールバックリクエストをキューに保存して後で処理決済処理、注文受付
代替サービスフォールバックバックアップサービスにルーティングCDN二重化、マルチリージョン
空レスポンスフォールバック空の結果を返却(エラーの代わりに)検索オートコンプリート、レコメンドウィジェット
手動切替フォールバック運用者が手動で代替ロジックを有効化重要なビジネスロジック

6.2 多段階フォールバック実装

@Service
@Slf4j
public class ProductRecommendationService {

    private final RecommendationEngine primaryEngine;
    private final RecommendationEngine secondaryEngine;
    private final RedisTemplate<String, List<Product>> cache;

    @CircuitBreaker(name = "recommendation",
                    fallbackMethod = "secondaryRecommendation")
    public List<Product> getRecommendations(String userId) {
        return primaryEngine.recommend(userId);
    }

    // 1次フォールバック:補助レコメンドエンジンを使用
    private List<Product> secondaryRecommendation(
            String userId, Throwable t) {
        log.warn("1次レコメンドエンジン障害、補助エンジンに切替: {}", t.getMessage());
        try {
            return secondaryEngine.recommend(userId);
        } catch (Exception e) {
            return cachedRecommendation(userId, e);
        }
    }

    // 2次フォールバック:キャッシュされたレコメンド結果を返却
    private List<Product> cachedRecommendation(
            String userId, Throwable t) {
        log.warn("補助レコメンドエンジンも障害、キャッシュ参照: {}", t.getMessage());
        List<Product> cached = cache.opsForValue()
                .get("recommendation:" + userId);
        if (cached != null && !cached.isEmpty()) {
            return cached;
        }
        return defaultRecommendation(userId, t);
    }

    // 3次フォールバック:人気商品デフォルトリストを返却
    private List<Product> defaultRecommendation(
            String userId, Throwable t) {
        log.warn("キャッシュもなし、デフォルト人気商品を返却");
        return List.of(
            Product.popular("BEST-001", "ベストセラー商品A"),
            Product.popular("BEST-002", "ベストセラー商品B"),
            Product.popular("BEST-003", "ベストセラー商品C")
        );
    }
}

7. Netflix HystrixからResilience4jへのマイグレーション

Netflix Hystrixは2018年にメンテナンスモードに入り、Spring Cloud 2020.0.0から公式サポートが中止された。既存のHystrix使用プロジェクトはResilience4jへのマイグレーションが必要だ。

7.1 Resilience4j vs Hystrix vs Istio比較

項目HystrixResilience4jIstio
メンテナンス状態メンテナンスモード(2018~)活発にメンテナンス中活発にメンテナンス中
設計思想OOP(HystrixCommand継承)関数型プログラミング(デコレーター)インフラベース(サイドカープロキシ)
モジュール構成オールインワン必要なモジュールのみ選択完全なサービスメッシュ
Spring Boot統合Spring Cloud NetflixネイティブSpring BootスターターKubernetes環境が必要
分離方式Thread Pool / SemaphoreSemaphore / Thread Poolコネクションプール / Outlier Detection
設定方式Java Config / PropertiesYAML / Java Config / アノテーションKubernetes CRD(YAML)
リアクティブサポート限定的(RxJava 1)完全サポート(Reactor、RxJava 2/3)該当なし
メトリクスHystrix DashboardMicrometer / PrometheusPrometheus / Kiali
フォールバックHystrixCommand.getFallback()fallbackMethodアノテーション未サポート(503返却)
学習曲線普通低い高い(サービスメッシュの理解が必要)

7.2 マイグレーションコアチェックリスト

ステップ1:依存関係の置換

// 削除
// implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'

// 追加
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'

ステップ2:コード変換パターン

HystrixResilience4j
@HystrixCommand(fallbackMethod = "fallback")@CircuitBreaker(name = "svc", fallbackMethod = "fallback")
HystrixCommand extends HystrixCommandDecorators.ofSupplier(() -> ...).withCircuitBreaker(cb)
@HystrixProperty(name = "...")application.yml設定
HystrixDashboardMicrometer + Grafana

ステップ3:設定マイグレーション

HystrixのcircuitBreaker.requestVolumeThresholdはResilience4jのminimumNumberOfCallsに対応し、circuitBreaker.errorThresholdPercentagefailureRateThresholdに対応する。circuitBreaker.sleepWindowInMillisecondswaitDurationInOpenStateに変換される。

ステップ4:段階的な移行

一度に全体を置換するのではなく、サービスごとに段階的にマイグレーションする。Resilience4jとHystrixは同一プロジェクトで共存可能なため、新サービスからResilience4jを適用し、既存サービスを順次移行するのが安全だ。


8. 障害事例分析と復旧手順

8.1 事例1:Retry Storm(リトライストーム)

状況:決済ゲートウェイ障害時に全クライアントが同時にリトライを実行し、ゲートウェイの復旧を遅延させた。

原因:CircuitBreakerなしにRetryのみ適用。リトライ間隔にjitter(ランダム遅延)がなく、同期化されたリトライが発生。

解決

  • CircuitBreakerをRetryと併用し、一定の失敗率以上でリトライ自体を遮断
  • Exponential backoffにjitterを追加
resilience4j:
  retry:
    instances:
      paymentGateway:
        maxAttempts: 3
        waitDuration: 1s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        enableRandomizedWait: true # jitter有効化
        randomizedWaitFactor: 0.5 # 50%範囲内でランダム化

8.2 事例2:Bulkhead未適用によるスレッドプール枯渇

状況:在庫確認APIが遅くなり、Tomcatスレッドプール全体を占有。決済、注文照会など無関係なAPIまでタイムアウトが発生。

原因:すべての外部サービス呼び出しが同一スレッドプールで実行。

解決

  • サービス別にThreadPool Bulkheadを適用してスレッドを分離
  • 遅いサービスがスレッドプール全体を占有できないように制限

8.3 事例3:サーキットブレーカーの閾値設定ミス

状況minimumNumberOfCalls: 1failureRateThreshold: 50に設定。たった1回の失敗でサーキットが開き、正常なサービスも遮断された。

原因:統計的に有意でない少数の呼び出しで状態遷移が発生。

解決

  • minimumNumberOfCallsを最低5~10に設定
  • slidingWindowSizeを十分に大きく設定(最低10以上)
  • 本番環境で実際のトラフィックパターンを分析した後に閾値を調整

8.4 復旧手順の標準化

#!/bin/bash
# circuit-breaker-recovery.sh
# サーキットブレーカー障害復旧手順スクリプト

echo "===== サーキットブレーカー状態確認 ====="
# Actuatorエンドポイントでサーキットブレーカー状態確認
curl -s http://localhost:8080/actuator/circuitbreakers | jq '.circuitBreakers'

echo ""
echo "===== ダウンストリームサービスヘルスチェック ====="
curl -s http://payment-service:8080/actuator/health | jq '.status'
curl -s http://inventory-service:8080/actuator/health | jq '.status'

echo ""
echo "===== サーキットブレーカー強制クローズ(ダウンストリーム復旧確認後) ====="
# 注意:ダウンストリームサービスが完全に復旧した後にのみ実行
# curl -X POST http://localhost:8080/actuator/circuitbreakers/paymentService/close

echo ""
echo "===== 現在のメトリクス確認 ====="
curl -s http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.state | jq '.'
curl -s http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.failure.rate | jq '.'

echo ""
echo "===== Istio Outlier Detection状態確認 ====="
kubectl get destinationrules -n production
kubectl describe destinationrule payment-service-circuit-breaker -n production

9. 運用モニタリングとメトリクス

9.1 主要モニタリングメトリクス

サーキットブレーカー運用で必ずモニタリングすべきメトリクスは以下の通り。

メトリクス説明警告閾値
resilience4j.circuitbreaker.state現在のサーキット状態(0=CLOSED, 1=OPEN, 2=HALF_OPEN)state == 1(OPEN)
resilience4j.circuitbreaker.failure.rate現在の失敗率(%)40%以上
resilience4j.circuitbreaker.calls成功/失敗/無視/遮断された呼び出し数遮断呼び出し急増時
resilience4j.circuitbreaker.slow.call.rate遅い呼び出しの比率(%)60%以上
resilience4j.bulkhead.available.concurrent.calls使用可能な同時呼び出し数0に近い場合
resilience4j.retry.callsリトライ回数急増時
resilience4j.ratelimiter.available.permissions使用可能な許可数0に近い場合

9.2 Prometheus + Grafanaダッシュボード設定

Resilience4jはMicrometerを通じてPrometheus形式のメトリクスを自動公開する。

# Prometheusスクレイプ設定
scrape_configs:
  - job_name: 'spring-boot-resilience4j'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['payment-service:8080']
        labels:
          application: 'payment-service'

Grafanaアラートルール例:サーキットがOPEN状態に遷移したらSlackアラートを送信するよう構成する。

# Grafana Alert Rule(プロビジョニング)
groups:
  - name: circuit-breaker-alerts
    rules:
      - alert: CircuitBreakerOpen
        expr: resilience4j_circuitbreaker_state{state="open"} == 1
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: 'サーキットブレーカーOPEN - {{ $labels.name }}'
          description: >
            {{ $labels.application }}の{{ $labels.name }}
            サーキットブレーカーがOPEN状態です。
            直ちにダウンストリームサービスの状態を確認してください。

      - alert: HighFailureRate
        expr: resilience4j_circuitbreaker_failure_rate > 40
        for: 30s
        labels:
          severity: warning
        annotations:
          summary: '高い失敗率を検知 - {{ $labels.name }}'
          description: >
            {{ $labels.name }}の失敗率が{{ $value }}%です。
            サーキットが開く前に原因を特定してください。

9.3 Istioモニタリング(Kiali + Grafana)

# Istioメッシュ内のサービス状態確認
istioctl proxy-config cluster <pod-name> -n production | grep outlier

# Envoy統計確認
kubectl exec -it <pod-name> -n production -c istio-proxy -- \
  curl localhost:15000/stats | grep outlier_detection

# Kialiダッシュボードアクセス
istioctl dashboard kiali

10. トラブルシューティング

サーキットブレーカーが開かない場合

  • minimumNumberOfCalls値を確認する。この値より少ない呼び出ししか発生していなければ、失敗率が100%でもサーキットは開かない。
  • recordExceptionsに実際に発生している例外タイプが含まれているか確認する。未登録の例外は失敗としてカウントされない。
  • ignoreExceptionsに意図せず障害例外が含まれていないか点検する。

サーキットがHALF-OPENからすぐにOPENに戻る場合

  • permittedNumberOfCallsInHalfOpenState値が小さすぎると、統計的に有意な判断が困難。最低3~5に設定する。
  • ダウンストリームサービスが部分的にしか復旧していない場合に発生し得る。ダウンストリームの完全な復旧を確認する。

Bulkhead関連エラー

  • BulkheadFullExceptionが頻発する場合はmaxConcurrentCalls値を増やすか、ダウンストリームサービスの応答時間を改善する。
  • ThreadPool Bulkhead使用時にqueueCapacityが0だと、スレッドプールが満杯の時に即座にリジェクトされる。

Istio Outlier Detectionが動作しない場合

  • PodにIstioサイドカープロキシがインジェクトされているか確認する:kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}'
  • DestinationRuleのhostフィールドが正確なサービスFQDNであるか確認する。
  • maxEjectionPercentが低すぎると、一部の異常インスタンスが除去されない場合がある。

11. 実践チェックリスト

設計段階

  • 各ダウンストリームサービスのSLA(応答時間、可用性)を確認したか
  • サービス別の障害影響度を分類したか(Critical / High / Medium / Low)
  • 障害時のフォールバック戦略を定義したか(キャッシュ、デフォルト値、キュー、代替サービス)
  • Retry適用対象が冪等性(Idempotency)を保証しているか確認したか
  • Retry + CircuitBreakerの組み合わせ使用を決定したか(Retry単独使用は禁止)
  • 合計タイムアウト = timeout * maxAttempts 値がユーザーSLA以内であるか確認したか

実装段階

  • CircuitBreakerのslidingWindowSizeminimumNumberOfCallsを十分に大きく設定したか(最低5~10)
  • recordExceptionsにネットワーク/タイムアウト関連の例外を登録したか
  • ignoreExceptionsにビジネス例外(400 Bad Requestなど)を登録したか
  • サービス別にBulkheadを分離適用したか
  • 外部API呼び出しにRate Limiterを適用したか
  • フォールバックメソッドのパラメータが元のメソッドと一致しているか確認したか(+ Throwable追加)

運用段階

  • Actuatorエンドポイント(/actuator/circuitbreakers/actuator/health)を公開したか
  • Prometheusメトリクス収集を設定したか
  • サーキットOPEN状態遷移時のアラート(Slack、PagerDutyなど)を設定したか
  • 失敗率警告閾値のアラートを設定したか
  • サーキットブレーカー障害復旧手順(Runbook)を文書化したか
  • 定期的なChaos Engineeringテスト(サービス障害注入)を実施しているか
  • Istio環境であればDestinationRuleとOutlier Detectionを設定したか

テスト段階

  • サーキット状態遷移(CLOSED -> OPEN -> HALF-OPEN -> CLOSED)シナリオをテストしたか
  • フォールバックメソッドが正常に動作するかテストしたか
  • Bulkhead満杯状態をシミュレーションしたか
  • ダウンストリームサービス完全ダウンシナリオをテストしたか
  • 遅い応答(Slow Call)シナリオをテストしたか

おわりに

レジリエンスパターンはマイクロサービスアーキテクチャにおいて選択ではなく必須だ。サーキットブレーカー、Bulkhead、Retry、Rate Limiter、Timeoutを適切に組み合わせれば、単一サービスの障害がシステム全体に伝播することを効果的に阻止できる。

核心原則をまとめると以下の通り。

  1. Retry単独使用は禁止:必ずCircuitBreakerと併用してリトライストームを防止する。
  2. サービス別分離:Bulkheadで各ダウンストリームサービスのリソース使用を分離する。
  3. 多段階フォールバック:単一フォールバックではなく、代替サービス -> キャッシュ -> デフォルト値の多段階構造を設計する。
  4. インフラ + アプリレベルの二重防御:IstioのOutlier DetectionとResilience4jを併用する。
  5. モニタリング必須:サーキット状態、失敗率、遅い呼び出し比率をリアルタイムモニタリングし、アラートを設定する。

HystrixからResilience4jへのマイグレーションは段階的に行い、新規サービスからResilience4jを導入するのが現実的だ。最も重要なのは、定期的なChaos Engineeringテストを通じて、設定したレジリエンスパターンが実際の障害状況で期待通りに動作するか検証することだ。


参考資料