【実務・中級編】 サーキットブレーカーパターンによる障害の連鎖防止 – Web APIアーキテクチャ・データ連携実践ガイド

障害の連鎖を断ち切れ:サーキットブレーカーパターンで守るWeb APIの「生存圏」

ネットワークエンジニアとして数々の修羅場をくぐり抜けてきた中で、最も忌むべき事態は「小さな局所的な障害が、システム全体を巻き込むドミノ倒しになること」だ。

外部APIのレスポンスが遅延し、その結果として自社のWebアプリケーションのスレッドが枯渇し、最終的にフロントエンドからバックエンドまで全滅する――この手の「連鎖的障害」を阻止する切り札こそが、今回解説するサーキットブレーカー(Circuit Breaker)パターンである。

1. サーキットブレーカーの「三つの状態」と生存戦略

電気回路のブレーカーと同じく、このパターンは障害を検知すると「電流(リクエスト)」を物理的に遮断する。状態遷移のロジックは以下の3つに集約される。

Closed(正常状態)

通常運用時。リクエストは滞りなく外部APIへと送られる。この段階では失敗率の閾値(Threshold)のみを監視し、カウントを続けている。

Open(遮断状態)

失敗率が閾値を超えた瞬間、ブレーカーが落ちる。ここから先は外部へのリクエストを一切行わず、即座にエラーレスポンス(あるいはキャッシュの返却)を呼び出し元に返す。「相手が死んでいるのに、無駄弾を撃ち続けるな」という、インフラ運用の鉄則を体現した状態だ。

Half-Open(半開状態)

一定時間(タイムアウト期間)が経過すると、システムは「相手は回復しただろうか?」と様子を見るために、ごく少数のみリクエストを通す。ここで成功が続けば Closed に戻り、失敗すれば再び Open に落ちる。

2. 実践:Pythonによる実装の勘所

世の中には便利なライブラリ(resilience4jやpybreakerなど)が溢れているが、まずはその概念を理解するために、Pythonで状態管理をシミュレートしてみよう。

import time

class CircuitBreaker:
    def __init__(self, failure_threshold=3, recovery_timeout=10):
        self.failure_count = 0
        self.state = "CLOSED"
        self.threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = 0

    def call(self, func, *args):
        if self.state == "OPEN":
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = "HALF-OPEN"
            else:
                raise Exception("Circuit is OPEN. Request blocked.")

        try:
            result = func(*args)
            self._reset()
            return result
        except Exception as e:
            self._handle_failure()
            raise e

    def _handle_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.threshold:
            self.state = "OPEN"

    def _reset(self):
        self.failure_count = 0
        self.state = "CLOSED"

# 使用例:外部API呼び出しを模した関数
def external_api_call():
    # ここで接続タイムアウトや5xxエラーが発生すると想定
    raise ConnectionError("Upstream service is down")

3. インフラ視点での「美しいAPI設計」とデバッグ手順

サーキットブレーカーを導入する際、最も重要なのが「クライアントに何を返すか」だ。

RFC 7231に準拠するなら、本来であれば 503 Service Unavailable を返すべきだが、サーキットブレーカーが発動している場合は、可能であれば Retry-After ヘッダーを付与することを強く推奨する。これにより、クライアントに対して「いつ再試行すべきか」という明確な指針を示すことができる。

デバッグのためのCLI Tips

障害時に、今ブレーカーがどうなっているかを確認するのはエンジニアの腕の見せ所だ。例えば curl で特定のヘッダーを監視する仕組みを組んでおくと便利だ。

# 外部APIのヘッダーを確認しつつ、接続時間を計測するコマンド
curl -v -o /dev/null -s -w "HTTP Status: %{http_code}\nTime: %{time_total}s\n" https://api.example.com/v1/resource

もし、この time_total が徐々に増え、最終的に 000(接続失敗)になるようなら、それはアプリ側でサーキットブレーカーをトリガーすべき明確なサインだ。

4. 現場の教訓:パラメータチューニングの罠

最後に、現場でよくある失敗談を一つ。

失敗率の閾値をあまりに厳しく(例えば3回失敗で即遮断)設定すると、一時的なネットワークの瞬断(フラッピング)だけでシステムが全停止してしまう。逆に緩すぎると、バックエンドのデータベースが死ぬまでリクエストを送り続け、復旧不可能な深手を負うことになる。

  • タイムアウト値の設計: 外部APIのレスポンスタイムアウトは、自社の許容範囲よりもわずかに短く設定する。
  • オブザーバビリティ: サーキットブレーカーの状態(Closed/Open/Half-Open)をメトリクスとしてPrometheus等に送り、Grafanaで可視化しておくこと。

「落ちることを前提にシステムを作る」。これが現代のクラウドネイティブなAPI設計における最強の防御だ。コードを書くときも、インフラを設計するときも、常に「最悪の事態」を想定したブレーカーを仕込んでおいてほしい。

皆さんのシステムが、予期せぬ障害の連鎖から守られることを祈っている。

コメント

タイトルとURLをコピーしました