システム障害の連鎖反応
マイクロサービスが単独で動作することは稀です。一般的なECサイトのフローでは、注文サービス(Order Service)が在庫サービス(Inventory Service)を呼び出し、それがさらにレガシーな倉庫データベースに問い合わせを行うかもしれません。もしそのデータベースがハングアップすると、在庫サービスはレスポンスを待つ間にワーカー・スレッドを占有し始めます。数秒のうちに注文サービス自体の200個の接続スレッドプールが枯渇し、チェックアウトプロセス全体が停止してしまいます。これが「連鎖的な障害(Cascading Failure)」です。
失敗したリクエストを闇雲にリトライしても、通常は状況を悪化させるだけです。ダウンストリームのサービスがすでに高負荷で苦しんでいる場合、さらにトラフィックを浴びせれば、確実にダウンしたままになります。サーキットブレーカー・パターンは、物理的な安全装置(ブレーカー)のように機能することで、この問題を解決します。サービスの異常を検知すると、即座に接続を遮断し、アーキテクチャの他の部分を保護します。
初めてのサーキットブレーカーの実装
サービスメッシュのような重厚なインフラがなくても、このロジックは実装可能です。最近のライブラリは、状態管理を肩代わりしてくれます。以下は、Pythonとcircuitbreakerライブラリを使用して、不安定な外部API呼び出しをラップする実践的な例です。
# pip install pycircuitbreaker
from circuitbreaker import circuit
import requests
# 5回連続で呼び出しに失敗した場合にサーキットを遮断
@circuit(failure_threshold=5, recovery_timeout=60)
def call_external_api():
# スレッドのハングアップを避けるため、厳格なタイムアウトを設定
response = requests.get("https://api.unreliable-service.com/data", timeout=0.5)
response.raise_for_status()
return response.json()
try:
data = call_external_api()
except Exception:
# サービスが失敗した、またはサーキットがすでに「OPEN(遮断)」状態の場合に実行
data = get_cached_backup_data()
このデコレータは関数を監視します。5回連続でリクエストが失敗すると、サーキットが「トリップ(遮断)」します。その後60秒間、この関数の呼び出しはすべて即座にエラーを返します。ネットワークリクエストすら行われません。これにより、負荷のかかっているサービスに、復旧やオートスケールのための1分間の「静止時間」を与えることができます。
レジリエンス(回復力)の3つの状態
サーキットブレーカーはステートマシンとして機能します。本番環境の問題をデバッグするには、これら3つのフェーズ間の遷移を理解することが重要です。
1. Closed(クローズ)状態:正常
リクエストは通常通り流れます。ブレーカーは、成功数に対する直近の失敗数を追跡します。エラー率が閾値(例:全トラフィックの5%)を下回っている限り、システムはこの状態を維持します。
2. Open(オープン)状態:即座に失敗
失敗の制限値に達すると、ブレーカーはOpenに切り替わります。すべての呼び出しは即座に失敗します。この「フェイルファスト(Fail-fast)」の仕組みは、ほぼ確実にタイムアウトするリクエストにアプリケーションのリソースを浪費するのを防ぐために不可欠です。
3. Half-Open(ハーフオープン)状態:様子見
リカバリ・タイムアウト(例:30秒や60秒)が経過すると、ブレーカーはHalf-Openに入ります。ここでは、ちょうど1回だけ「試行(プローブ)」リクエストを通します。その1回のリクエストが成功すれば、ブレーカーは**Closed**にリセットされます。失敗すれば、タイマーが再起動し、ブレーカーは**Open**のままとなります。
このパターンを導入したことで、私たちのチームのトラフィック急増への対応は一変しました。重要度の低いレコメンデーション・エンジンが故障した際にプラットフォーム全体がダウンするのではなく、サーキットが遮断されることで、主要なショッピングカート機能は完全に動作し続けるようになりました。
本番環境向けの微調整
高トラフィックな環境では、単純なカウンターだけでは不十分な場合があります。リクエストの量や発生しているエラーの種類を考慮する必要があります。
回数だけでなく「割合」を使用する
秒間1,000リクエストの場合、5回の失敗は統計的に無視できるレベルです。しかし、秒間5リクエストの場合、5回の失敗は完全な停止を意味します。Resilience4jやPollyのようなライブラリを使用して、failureRateThreshold(失敗率の閾値)を設定しましょう。本番環境の一般的な基準は、10秒間のスライディングウィンドウ内でリクエストの50%が失敗した場合(かつ最低20リクエスト以上のボリュームがある場合)にサーキットを遮断することです。
フォールバックの威力
サーキットブレーカーは、フォールバック戦略と組み合わせたときに最も効果を発揮します。依存しているサービスがダウンしている場合、コードは「十分に許容できる」代替案を提供すべきです。レコメンデーション・サービスなら「人気の商品」の静的リストを返し、ユーザープロフィール・サービスならキャッシュされたデータを返します。これにより、バックエンドが一部破損していても、ユーザー体験をスムーズに保つことができます。
実践チェックリスト
効果的な実装には、単に関数をラップする以上のことが必要です。以下の4つのルールを念頭に置いてください。
- 積極的なタイムアウトを優先する: APIの平均レスポンス時間が100msなら、タイムアウトは300msに設定しましょう。失敗を待つのに10秒もかけていては、サーキットブレーカーが効率的に機能しません。
- 状態を可視化する: ブレーカーのメトリクスをGrafanaに流し込みましょう。サーキットが「フラッピング(OpenとClosedを頻繁行き来すること)」しているときは、ネットワークの不安定さや閾値の感度が良すぎることを示しているため、即座に把握する必要があります。
- クライアントエラーを除外する: HTTP 400、401、404エラーでサーキットを遮断してはいけません。これらはユーザーのミスや不正なデータであり、サービスの故障ではありません。5xxエラーとネットワークタイムアウトのみを失敗としてカウントしてください。
- カオスエンジニアリングのテスト: Toxiproxyなどのツールを使用して、ステージング環境で500msのレイテンシや20%のパケットロスをシミュレートしましょう。実際のインシデントが発生する前に、サーキットブレーカーが作動し、フォールバックが機能することを確認してください。
このパターンの採用には、考え方の転換が必要です。「ネットワークは信頼できる」という前提を捨てなければなりません。失敗を前提とした設計を行うことで、一つの遅延した依存関係がインフラ全体を崩壊させるのを防ぐことができるのです。

