カーニバルの時間が始まりました。🎉

ホームパーティーを開催いたします。バンガロール市内および近郊にお住まいの方はぜひご参加ください。ただし、条件が1つあります:

遅刻はご遠慮ください。

誰しも、いつも遅れてくる友人が1人いるものです。それでも招待はします。そして、スマホを何度も確認することになります。

「もうすぐ来るはずだ」

その間、周囲の人は立ち尽くし、計画は中断し、料理は冷め、氷は溶け、忍耐は徐々に失われていきます。やがて問題は、遅れてくる友人そのものではなくなります。

問題は、みんなが遅れてくる友人をいつまでも待っていることです。

驚くべきことに、分散システムにもまったく同じ問題があります。あなたのAgentic Applicationsもその例外ではありません。🤖

あるサービスが遅くなったり障害を起こしたりすると、他のサービスは待機を続けます。

  • リクエストがオープン状態のままになる。
  • スレッドが占有され続ける。
  • コネクションが消費される。
  • キューが肥大化する。

最終的に、1つのサービスの問題が、健全な他のサービスにまで波及します。そこで登場するのがサーキットブレーカーパターンです。

サーキットブレーカーは文字通りその名の通りです。仕組みがわからない場合は、電気技師に説明してもらいましょう。😎


電気工学におけるサーキットブレーカー ⚡

電気工学のB.Techを辛うじて卒業した私は、Circuit Breakerという言葉がデザインパターンとして登場するのを初めて見たとき、本当に怖気づきました。

頭の中には電源系統、変圧器、電流、電圧、そして期末試験の問題が蘇りました。💀

しかし面白いことに、ソフトウェアのサーキットブレーカーは電気工学で学ぶのと同じ考え方を借りています。重要な状態は3つあります:

Closed、Open、Half-Open

  • 閉回路(closed circuit)では電流が正常に流れます。スイッチはONで、電球に電力が届きます。
  • 開回路(open circuit)では回路が切断され、電流は流れず、電球は消灯します。

Real Electrical Circuit for Analogy

「Half-Open」は電気工学の標準的な状態ではありません。ソフトウェアの文脈では、テスト状態と捉えます。障害発生後、即座に全量を復帰させるのではなく、一部だけを慎重に流してシステムが回復したかどうかを確認します。問題なければ回路を閉じ、障害が残っていれば再び開きます。

このアナロジーを踏まえて、分散システムの世界に移りましょう。


分散システムにおけるサーキットブレーカー 🧱

カーニバルの夜なので、当然ピザが必要です。🍕

PartyServicePizzaService を呼び出して注文を送信するケースを考えます。正常時は次のようになります:

Host
 │
 ▼
PartyService
 │
 ▼
PizzaService
 │
 ▼
Pizza on the way ✅

Enter fullscreen mode Exit fullscreen mode

ホストが注文を送信し、PizzaService が受け付け、PartyService が確認を受け取ります。全員が幸せに食事をします。

しかし、PizzaService が不調になった場合はどうなるでしょうか?

  • キッチンが混雑している(カーニバルの夜ですから)。
  • サービスがクラッシュしている。
  • ネットワークが遅い。
  • 金曜の夜にデプロイを決行した誰かがいる… 👀

このときの流れは次のようになります:

PartyService
 │
 ▼
PizzaService
 │
 ▼
Request Timeout ❌

Enter fullscreen mode Exit fullscreen mode

1件の注文が数秒待機するだけなら大した問題ではありません。しかし、数千件の注文が同時に発生したらどうでしょうか:

1000 Orders
 │
 ▼
Slow PizzaService
 │
 ▼
Threads Waiting
 │
 ▼
Connections Waiting
 │
 ▼
Queues Growing
 │
 ▼
Resources Consumed

Enter fullscreen mode Exit fullscreen mode

ここで興味深いことが起こります。PizzaService は不健全ですが、PartyService も不健全になるのです。


本当の問題:カスケード障害 🌊

次のアーキテクチャを考えてみましょう:

                Host
                 │
            PartyService
              ╱       ╲
     PizzaService     DrinkService

Enter fullscreen mode Exit fullscreen mode

PizzaService がダウンすると、PartyService は呼び出しを続けます。注文は待機し続け、来場者は増え、スレッドは占有され続け、ついにはPartyService 自身もリソースを枯渇させます。

PizzaService ❌
 │
 ▼
PartyService ❌
 │
 ▼
Whole Application ❌

Enter fullscreen mode Exit fullscreen mode

1つのサービスが障害を起こしただけで、障害がシステム全体に伝播しました。これがカスケード障害と呼ばれる現象であり、サーキットブレーカーが防ごうとしているものです。


サーキットブレーカーの仕組み 🚦

サーキットブレーカーは、サービスとリモート依存関係の間に配置されます:

PartyService → Circuit Breaker (watches every call) → PizzaService

Enter fullscreen mode Exit fullscreen mode

  • 健全な状態では、リクエストは通常通り通過します。
  • 依存関係が繰り返し障害を起こすと、サーキットブレーカーは「しばらくそのサービスへの呼び出しを停止します」と判断します。

これにより、3つの状態が実現されます。

1. Closed 状態 🟢

通常の状態です。リクエストは通過し、ブレーカーは監視を続けます。

Request 1 → ✅
Request 2 → ✅
Request 3 → ❌ (network fail, packet drop, kitchens have bad days)
Request 4 → ✅

Enter fullscreen mode Exit fullscreen mode

少数の障害はサービスが死んだことを意味しません。したがって、障害率が許容範囲内であれば回路はClosedのままです。

2. Open 状態 🔴

障害が積み重なると:

Request 1 → ❌
Request 2 → ❌
Request 3 → ❌
Request 4 → ❌

Enter fullscreen mode Exit fullscreen mode

やがてブレーカーは「何かおかしい」と判断します。😅

CLOSED --(too many failures)--> OPEN

Enter fullscreen mode Exit fullscreen mode

するとPizzaService へのリクエスト送信を完全に停止します。

Request → Circuit Breaker → ❌ → Fail Fast / Fallback

Enter fullscreen mode Exit fullscreen mode

リクエストは即座に失敗します。不要なネットワーク呼び出しやタイムアウト待ち、すでに不健全なサービスへの負荷増加を防げます。

3. Half-Open 状態 🟡

しかし、回路を永遠にOpenのままにしておくわけにはいきません。PizzaService が回復している可能性もあります。一定時間待機した後、ブレーカーはHalf-Openに移行し、少数のテストリクエストを許可します。

Test Request 1 → ✅
Test Request 2 → ✅ → back to CLOSED 🟢

Test Request 1 → ❌ → back to OPEN 🔴

Enter fullscreen mode Exit fullscreen mode

Half-Openが重要な理由は、サービスを盲目的に信頼せず、まずテストする点にあります。


完全なフロー

        CLOSED 🟢
           │
           │  too many failures
           ▼
         OPEN 🔴
           │
           │  wait timeout elapses
           ▼
      HALF-OPEN 🟡
       │       │
  success    failure
       │       │
       ▼       ▼
   CLOSED 🟢  OPEN 🔴

Enter fullscreen mode Exit fullscreen mode

メンタルモデルはシンプルです:

Closed → リクエストを通過させる
Open → 依存関係への呼び出しを停止する
Half-Open → 回復したかどうかを慎重にテストする

では実際に作ってみましょう。


Javaでサーキットブレーカーを作ってみる

内部の動作を理解せずにライブラリを使うのは、ブレーキの仕組みを知らずに車を運転するようなものです。動くうちはいいですが…😅

まず状態を定義します:

public enum CircuitState {
    CLOSED,
    OPEN,
    HALF_OPEN
}

Enter fullscreen mode Exit fullscreen mode

次にブレーカー本体です。現在の状態、障害回数、閾値、Open状態の継続時間、Openになった時刻を管理します。

import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class CircuitBreaker {

    private final int failureThreshold;
    private final long openWaitMillis;

    private final AtomicReference<CircuitState> state =
            new AtomicReference<>(CircuitState.CLOSED);
    private final AtomicInteger failureCount = new AtomicInteger(0);
    private volatile long openedAt = 0;

    public CircuitBreaker(int failureThreshold, long openWaitMillis) {
        this.failureThreshold = failureThreshold;
        this.openWaitMillis = openWaitMillis;
    }

    public <T> T execute(Callable<T> call, Callable<T> fallback) throws Exception {
        if (state.get() == CircuitState.OPEN) {
            if (System.currentTimeMillis() - openedAt >= openWaitMillis) {
                state.set(CircuitState.HALF_OPEN);
            } else {
                return fallback.call(); // fail fast
            }
        }

        try {
            T result = call.call();
            onSuccess();
            return result;
        } catch (Exception e) {
            onFailure();
            return fallback.call();
        }
    }

    private void onSuccess() {
        failureCount.set(0);
        state.set(CircuitState.CLOSED);
    }

    private void onFailure() {
        int failures = failureCount.incrementAndGet();
        if (failures >= failureThreshold) {
            state.set(CircuitState.OPEN);
            openedAt = System.currentTimeMillis();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Open状態では、リモートサービスを呼び出さず、即座に失敗させる点に注目してください。

new CircuitBreaker(3, 10_000) と設定した場合:

3 failures       → OPEN
Wait 10s         → HALF_OPEN
Test succeeds    → CLOSED
Test fails       → back to OPEN

Enter fullscreen mode Exit fullscreen mode

使用例は次のようになります:

PizzaOrder order = breaker.execute(
    () -> pizzaService.order(guestId),
    () -> PizzaOrder.fallback(guestId)
);

Enter fullscreen mode Exit fullscreen mode

閾値に達すると、次の注文はPizzaService に到達せず、即座に失敗します。依存関係が不健全なので、トラフィックを停止するのです。


ただし、本番環境向けではありません 👀

このクラスを本番サービスにコピーする前に… やめておきましょう。😅

これは概念を理解するための最低限のものです。実際のサーキットブレーカーは障害率、スライディングウィンドウ、並行性、タイムアウト、スローコール、例外フィルタリング、メトリクス、フォールバックなどを扱います。

たとえば「50回の障害」は本当に深刻でしょうか?

1000 successful requests + 50 failures  → 恐らく問題ない
Last 100 calls: 50 failures (50%)       → 明らかに問題

Enter fullscreen mode Exit fullscreen mode

生のカウントより、直近の呼び出しウィンドウの方がはるかに意味があります。そのため、本番環境では通常ライブラリを利用します。


サーキットブレーカー vs リトライ vs タイムアウト

「障害が発生したらリトライすればいいのでは?」と思うかもしれません。

リトライは別の問題を解決します。

  • リトライは「一時的な障害かもしれない。もう一度試そう」と考えます。
  • サーキットブレーカーは「十分に障害が発生したので、この依存関係への呼び出しを停止する」と判断します。

PizzaService がすでに過負荷状態で、10,000件の注文があり、すべての注文が3回リトライするとします。すると、苦しんでいるサービスに余計なリクエストが何千件も叩き込まれることになります。これは、遅れてくる友人のドアを4回ノックするようなものです。😂

分散システムでは、それぞれ異なる役割を持つパターンを組み合わせます:

パターン 解決する質問
Timeout どれだけ待つことを許容するか?
Retry もう一度試すべきか?
Circuit Breaker このサービスへの呼び出しを停止すべきか?

GenAIアプリにも必要です 💎

ここが多くの人が見落としがちなポイントです。

この記事のきっかけとなった実話 ❤️‍🔥

私自身、数週間前にこの問題を見逃していました。そして、それがこの記事を書く本当の理由です。

私はモデルオーケストレーターを構築していました。これは、1つのリクエストが単一のLLMを呼び出すのではなく、複数のLLM呼び出しをパイプラインで通過してから最終回答を返すシステムです:

Request
   │
   ▼
Router LLM   → ユーザーの意図を解析
   │
   ▼
Agent LLM    → 実際のタスクを実行(検索、生成、計算など)
   │
   ▼
Judge LLM    → エージェントの回答を最終確認
   │
   ▼
Final Response

Enter fullscreen mode Exit fullscreen mode

レストランの注文を3人のスタッフが順に処理するようなものです。誰か1人が何もせず、空の皿を次の人に回すと、客は「何か」受け取れますが、注文したものではありません。

実際に起きたのは、まさにそれでした。

ユーザーが奇妙で一貫性のない回答を受け取り始めました。最初の30分間、私は十分に警戒していませんでした。モニタリングでは92%のリクエストが200 OKを示していたからです。ほとんどのシステムでは200ステータスは「正常に動作した」ことを意味します。そのため、私はすべてが問題ないと判断しました。

しかし違いました。ユーザーが不良回答を報告し続け、ステータスコードだけでなく実際のレスポンスペイロードを確認したところ、監視していなかったフィールドにconfidence = 0という値があることがわかりました。

内部で実際に起きていたことは次の通りです:

Router LLM   ──fails/times out──▶  サイレントフォールバック
Agent LLM    ──fails/times out──▶  サイレントフォールバック
Judge LLM    ──fails/times out──▶  サイレントフォールバック
                                          │
                                          ▼
                          Response: 200 OK, confidence = 0
                     (ログ上は健全に見える — 「クラッシュ」していない)

Enter fullscreen mode Exit fullscreen mode

パイプラインの各ステージが障害を起こし、黙ってデフォルト応答にフォールバックしていました。例外も、500エラーも、アラートもありませんでした。ただ、技術的には成功したHTTPレスポンスに役に立たない回答が含まれていただけです。

私は1つのLLM呼び出しが失敗した場合のフォールバック処理は実装していましたが、チェーン全体が同時に失敗するケースは想定していませんでした。根本原因は私のコードにはなく、その頃に429 Too Many Requestsエラーが発生し始め、Anthropicのステータスページでその日のモデルAPI障害が確認されました。

修正として、コントローラーレベル(3つのLLM呼び出しの前段)にサーキットブレーカーを導入しました。実際のトラフィックをパイプラインに通す前に、ブレーカーは1トークンだけの軽量なプローブリクエストを送信し、LLMが実際に呼び出しを受け付けて応答していることを確認します。プローブが失敗するとブレーカーは即座にOpenになり、パイプライン全体が壊れた回答を成功として返すのではなく、明確なエラーで即座に失敗するようになります。

マルチエージェントシステムの真の危険性は、障害が必ずしもクラッシュのように見えない点です。3つのフォールバックが静かに嘘をつく合意をするように見えることもあるのです。


GenAIは新しい火です。誰もがLLMプロバイダー(OpenAI、Anthropic、Gemini、セルフホストモデルなど)の上に構築しています。そして、それらの呼び出しはすべて… 遅延したり、レート制限されたり、ダウンしたりする可能性のあるリモート依存関係へのネットワーク呼び出しです。

聞き覚えがありませんか?

LLM呼び出しは、まさに究極の遅れてくる友人です:

  • レスポンスに何秒もかかることがある(トークンを1つずつストリーミング)。
  • レート制限に達するとプロバイダーは429 Too Many Requestsを返す。
  • 1つのエージェントステップが多数のモデル呼び出しにファンアウトする(ツール、リトライ、リフレクショーループ)ため、1つの不安定な依存関係が急速に拡大する。

典型的な素朴な構成を想像してみてください:

Your AI App → prompt fails or times out → Retry ×3 → Retry ×3 → already rate-limited LLM provider

Enter fullscreen mode Exit fullscreen mode

レート制限されたモデルへのリトライは、すでに火災が発生しているキッチンにさらにピザを注文するようなものです。リトライのたびにレート制限の深みにはまり、レイテンシが爆発し、トークン料金が静かに増加します。

モデルクライアントの前にサーキットブレーカーを配置すると、次のようになります:

       AI Request
           │
           ▼
     Circuit Breaker
     ╱      │      ╲
 CLOSED   OPEN   HALF-OPEN
    │       │        │
    ▼       ▼        ▼
  LLM    Fallback  Test call
Provider            to LLM

Enter fullscreen mode Exit fullscreen mode

GenAIアプリにおけるフォールバックは、工夫の余地があります:

Model unavailable
├── Return a cached / similar answer
├── Drop to a smaller, cheaper model
├── Return a "please try again shortly" message
└── Queue the request for later processing

Enter fullscreen mode Exit fullscreen mode

したがって、次にエージェントが5秒間に40回モデルを呼び出すループが発生したとき、最先端のAIであっても、プロバイダーのドアをノックし続ける前にサーキットブレーカーが必要であることを思い出してください。

同じ古いパターン、新しい火。


フォールバックは何をすべきか 🤔

フォールバックはreturn nullを意味しません。やめましょう。

Dependency Down
├── Return cached data
├── Queue the request
├── Drop to a cheaper path
└── Return a graceful error

Enter fullscreen mode Exit fullscreen mode

重要なルール:成功を偽装しない。ピザの注文が失敗した場合、フォールバックが「何か」を返さなければならないからといって、ゲストに「Order Confirmed ✅」と伝えてはいけません。

優雅な失敗は、成功した嘘よりもはるかに優れています。


重要な質問 🚨

すべての例外で回路を開くべきでしょうか? いいえ。

400 Bad Request(ゲストが不正な入力を送信した)は、サービスが不健全であることを意味しません。しかし、次のようなものは強いシグナルです:

Connection refused
Timeout
503 Service Unavailable
429 Too Many Requests

Enter fullscreen mode Exit fullscreen mode

したがって、ブレーカーを設定する際に最も重要な質問の1つは次の通りです:

どの障害を実際にカウントすべきか?

その判断はアプリケーション固有です。


遅れてくる友人を迎え入れる 😊

  • Closed 🟢 — 友人:「あと5分で着くよ」 みんな:「OK、👍」 パーティーは続行。
  • Open 🔴 — 「5分」→「もうすぐ」→「渋滞」→「駐車場」で2時間経過した後、ついに「もう誰も待たない」と宣言。パーティーは友人を置いて進む。
  • Half-Open 🟡 — 後で:「ねえ、着いた?」→「着いたよ」=パーティー続行。「あと5分」=再びドアを閉じる。

これがサーキットブレーカーです。


最終的な要点

サーキットブレーカーと聞いたら、3つの状態を思い出してください:

CLOSED    → リクエストは通常通り流れる
OPEN      → リクエストは即座に停止
HALF-OPEN → 少数のリクエストで回復をテスト

Enter fullscreen mode Exit fullscreen mode

あるいは、バンガロールのホームパーティー風に言うと:

友人が来る → みんな待つ → 友人が失敗し続ける → 待つのをやめる → パーティー続行 😎

Enter fullscreen mode Exit fullscreen mode

サーキットブレーカーは障害のあるサービスを修復しません。サービスが障害を起こしているに、システムの残りを保護します — それがピザ屋でも、決済APIでも、お気に入りのLLMでも同じです。

障害は避けられない。カスケード障害は避けられる。

アウトロ 💚

ここまで読んでくださった方、本当にありがとうございます。🎉

分散システムは、あの遅れてくる友人のようなものです — 障害は避けられませんが、どのように対処するかはあなた次第です。サーキットブレーカーは壊れたサービスを修復しませんが、1つの不良な依存関係がシステム全体(またはパーティー全体)を巻き添えにするのを防ぎます。

この記事の目的は、 intimidating に聞こえるパターンを、3つの状態とタイマーとして見れば本当にシンプルであることを示すことでした。ピザの注文でも、決済APIでも、LLM呼び出しでも — 同じルールが適用されます:待つのをやめ、即座に失敗し、信頼する前に回復をテストする。

分散システムに携わるほど、最も有用なアイデアはシンプルであることに気づきます:

  • 待つのをやめる
  • 即座に失敗する
  • 信頼する前に回復をテストする

そして答えが「No」なら… 回路を開く

今後も分散システムパターンやバックエンドエンジニアリングについて、実運用システムで実際に現れるような内容を書いていく予定です。

それまでは、遅れてくる友人を待たないシステムを構築してください。😄

組織を運営されていて、私に記事やビデオチュートリアルの作成を依頼したい場合は、ぜひご連絡ください 🤝