はじめに
マイクロサービスアーキテクチャにおいて、サービス間呼び出しは本質的に不安定だ。ネットワーク遅延、タイムアウト、ダウンストリームサービスの障害は日常的に発生し、適切な防御メカニズムがなければ、単一サービスの障害がシステム全体に伝播する**連鎖障害(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 Bulkhead | ThreadPool 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比較
| 項目 | Hystrix | Resilience4j | Istio |
| ------------------------ | ---------------------------- | ------------------------------------ | -------------------------------------- |
| **メンテナンス状態** | メンテナンスモード(2018~) | 活発にメンテナンス中 | 活発にメンテナンス中 |
| **設計思想** | OOP(HystrixCommand継承) | 関数型プログラミング(デコレーター) | インフラベース(サイドカープロキシ) |
| **モジュール構成** | オールインワン | 必要なモジュールのみ選択 | 完全なサービスメッシュ |
| **Spring Boot統合** | Spring Cloud Netflix | ネイティブSpring Bootスターター | Kubernetes環境が必要 |
| **分離方式** | Thread Pool / Semaphore | Semaphore / Thread Pool | コネクションプール / Outlier Detection |
| **設定方式** | Java Config / Properties | YAML / Java Config / アノテーション | Kubernetes CRD(YAML) |
| **リアクティブサポート** | 限定的(RxJava 1) | 完全サポート(Reactor、RxJava 2/3) | 該当なし |
| **メトリクス** | Hystrix Dashboard | Micrometer / Prometheus | Prometheus / 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:コード変換パターン**
| Hystrix | Resilience4j |
| ---------------------------------------------- | ------------------------------------------------------------ |
| `@HystrixCommand(fallbackMethod = "fallback")` | `@CircuitBreaker(name = "svc", fallbackMethod = "fallback")` |
| `HystrixCommand extends HystrixCommand` | `Decorators.ofSupplier(() -> ...).withCircuitBreaker(cb)` |
| `@HystrixProperty(name = "...")` | `application.yml`設定 |
| `HystrixDashboard` | Micrometer + Grafana |
**ステップ3:設定マイグレーション**
Hystrixの`circuitBreaker.requestVolumeThreshold`はResilience4jの`minimumNumberOfCalls`に対応し、`circuitBreaker.errorThresholdPercentage`は`failureRateThreshold`に対応する。`circuitBreaker.sleepWindowInMilliseconds`は`waitDurationInOpenState`に変換される。
**ステップ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: 1`、`failureRateThreshold: 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の`slidingWindowSize`と`minimumNumberOfCalls`を十分に大きく設定したか(最低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テストを通じて、設定したレジリエンスパターンが実際の障害状況で期待通りに動作するか検証することだ。
参考資料
- [Resilience4j公式ドキュメント - Getting Started](https://resilience4j.readme.io/docs/getting-started-3)
- [Baeldung - Guide to Resilience4j With Spring Boot](https://www.baeldung.com/spring-boot-resilience4j)
- [Istio公式ドキュメント - Circuit Breaking](https://istio.io/latest/docs/tasks/traffic-management/circuit-breaking/)
- [Resilience4j vs Hystrix比較](https://resilience4j.readme.io/docs/comparison-to-netflix-hystrix)
- [Istio DestinationRule APIリファレンス](https://istio.io/latest/docs/reference/config/networking/destination-rule/)
- [Martin Fowler - Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
- [freeCodeCamp - Build Your Own Circuit Breaker in Spring Boot](https://www.freecodecamp.org/news/how-to-build-your-own-circuit-breaker-in-spring-boot-and-really-understand-resilience4j/)
현재 단락 (1/454)
マイクロサービスアーキテクチャにおいて、サービス間呼び出しは本質的に不安定だ。ネットワーク遅延、タイムアウト、ダウンストリームサービスの障害は日常的に発生し、適切な防御メカニズムがなければ、単一サービ...