分散システムに潜むカオス
モノリスなアプリケーションでは、メソッド呼び出しはローカルな出来事です。それは成功するか失敗するかのどちらかです。しかし、マイクロサービスには「ネットワーク」というカオスな変数が加わります。サービスAがサービスBを呼び出す際、パケットロスやDNSの不調、スレッドプールの飽和といった事態に翻弄されることになります。もしサービスAが遅延している依存先を無期限に待ち続ければ、そのスレッドは占有されたままになります。数分以内にクラスター全体が停止してしまう可能性もあります。これは単なるバグではなく、99.9%のアップタイム目標を台無しにする「連鎖的な障害(cascading failure)」です。
現在は非推奨となったNetflix Hystrixからいくつかの高トラフィックシステムを移行させた後、私のチームはResilience4jが優れた選択肢であることを見出しました。これはJava 8と関数型プログラミング向けに構築された軽量なライブラリです。1つの苦境にあるサービスがシステム全体の重荷にならないようにするためのセーフティネットを提供します。私たちの本番環境では、これらのパターンを実装することで、ピーク時の断続的なエラー率が40%近く減少しました。
Resilience4jエコシステムのセットアップ
統合は非常に簡単です。Resilience4jはスタンドアロンのライブラリとしても動作しますが、ほとんどのチームにとってはSpring Boot starterを使用するのが最適です。これにより、ロジックをBeanにハードコーディングするのではなく、application.ymlを通じて設定を管理できるようになります。
以下をpom.xmlに追加してください。AOPの依存関係は必須であることに注意してください。これがないと、アノテーションがサイレントに失敗し、機能しなくなります。
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOPによって、ライブラリはメソッド呼び出しをインターセプトできるようになります。ビジネスロジックをレジリエンス(回復力)のデコレータでラップすることで、コードをtry-catchブロックの山にすることなく機能を実装できます。
実践的な構成:現実世界の設定
デフォルト設定が本番環境で十分であることは稀です。感度と安定性のバランスを取る必要があります。レジリエンスを支える3つの柱をどのように設定するか見ていきましょう。
1. Circuit Breaker
これを「スマートなヒューズ」と考えてください。失敗率が特定の割合に達すると、サーキットが「オープン(開いた)」状態になります。それ以降の呼び出しはすべて即座に失敗(fail fast)します。これにより、ダウンストリームのサービスはさらなるリクエストに叩かれることなく、回復するための余裕を得ることができます。
resilience4j.circuitbreaker:
instances:
inventoryService:
registerHealthIndicator: true
slidingWindowSize: 20
permittedNumberOfCallsInHalfOpenState: 5
slidingWindowType: COUNT_BASED
minimumNumberOfCalls: 10
waitDurationInOpenState: 30s
failureRateThreshold: 50
この例では、直近20回の呼び出しを監視します。10回以上(50%)が失敗した場合、サーキットがトリップします。waitDurationInOpenState(オープン状態の待機時間)は少なくとも30秒に設定することをお勧めします。この期間が短いと、サーキットが開閉を頻繁に繰り返す「フラッピング」が発生し、実際の回復を妨げることがあります。
2. 指数バックオフを用いたスマートなリトライ
すべてのエラーが致命的とは限りません。「503 Service Unavailable」は一時的な不調かもしれません。しかし、即座にリトライを行うと「リトライストーム(retry storm)」を引き起こす可能性があります。これは、何千ものクライアントが全く同じタイミングでリトライを実行し、自社のサーバーを実質的にDDoS攻撃してしまう現象です。
resilience4j.retry:
instances:
inventoryService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
この設定では、試行の間隔を段階的に広げています。1回目のリトライは500ミリ秒後、2回目は1000ミリ秒後に行われます。この段階的なアプローチは、高コンカレンシー(高並行)時のシステム健全性を回復させるために不可欠です。
3. Rate Limiter
Circuit Breakerが他者から自分を守るものであるのに対し、Rate Limiterは自分自身、あるいはクライアントから自分を守るためのものです。これを使用して、バグのあるフロントエンドのループがバックエンドのリソースをすべて消費してしまうのを防ぎます。
resilience4j.ratelimiter:
instances:
inventoryService:
limitForPeriod: 100
limitRefreshPeriod: 1s
timeoutDuration: 0
ここでは、1秒あたり100リクエストを許可しています。timeoutDurationを0に設定することで、上限を超えたトラフィックを即座に拒否するようシステムに指示しています。キューで待機させることは、多くの場合ボトルネックをさらに上流に移動させるだけに過ぎません。
実装とアノテーションの順序
これらのパターンの適用は、ServiceレイヤーやClientレイヤーにアノテーションを追加するだけで完了します。ただし、実行順序が非常に重要です。デフォルトでは、Resilience4jはRetryをCircuit Breakerの「外側」に適用します。
@Service
public class InventoryClient {
@CircuitBreaker(name = "inventoryService", fallbackMethod = "getInventoryFallback")
@Retry(name = "inventoryService")
@RateLimiter(name = "inventoryService")
public String checkStock(String productId) {
return restTemplate.getForObject("/api/stock/" + productId, String.class);
}
public String getInventoryFallback(String productId, Exception e) {
// 製品 {} の在庫サービスを利用できません。エラー: {}
logger.error("Inventory service unavailable for product {}. Error: {}", productId, e.getMessage());
return "在庫状況不明";
}
}
Retryが外側にある場合、Circuit Breakerが1回の失敗を記録する前に、3回の試行が行われます。通常、これが望ましい動作です。これにより、複数の異なるリクエストにわたってリトライが失敗したときにのみ、サーキットがトリップするようになります。
オブザーバビリティ(観測性):勘に頼らない運用
沈黙したままトリップするCircuit Breakerは、DevOpsにとって悪夢です。これらのイベントをモニタリングスタックに公開する必要があります。Resilience4jは、Spring Boot ActuatorやMicrometerとネイティブに統合されています。
設定で必要なエンドポイントを公開します:
management:
endpoints:
web:
exposure:
include: health, metrics, resilience4jevents
health:
circuitbreakers: enabled
有効化すると、これらのメトリクスをPrometheusに取り込むことができます。resilience4j_circuitbreaker_stateというメトリクスを探してください。Grafanaでは、サーキットが5分以上「Open」状態のままになった場合にアラートを飛ばすよう設定しています。これは通常、手動の介入が必要な大規模なダウンストリームの停止を示しています。
堅牢なシステムを構築するということは、失敗は避けられないものであると受け入れることです。これら3つのパターンを組み合わせることで、脆弱なアーキテクチャを堅牢なものへと変貌させることができます。足元のネットワークが不安定であっても、サービスは稼働し続けるでしょう。

