circuit breakerの実装:Java・C#・Go・Node.jsの設定コードと閾値の決め方

circuit breakerの実装:Java・C#・Go・Node.jsの設定コードと閾値の決め方

circuit breakerは、呼び出し先の失敗が続いたときに呼び出しそのものを止め、待機時間のあとで少量だけ試して復旧を確かめる仕組みです。この記事では概念の説明は最小限にとどめ、Resilience4j・Polly・gobreaker・opossumの4ライブラリで実際に動く設定コードを並べます。あわせて、既定値のまま本番に出すと遮断が起きない理由、閾値を秒間リクエスト数から逆算する手順、IstioやAmazon ECSの「サーキットブレーカー」との違いも扱います。仕組みの全体像と導入判断はサーキットブレーカーの定義と導入判断をまとめた記事にまとめているので、先にそちらを読むと理解が早いはずです。版番号と既定値は2026年9月29日に各公式ドキュメントで確認した内容です。

まとめ:circuit breakerを実装する前に決める5つの値と置き場所

実装の前に決めるのは5つの値です。失敗率の閾値、判定に使う窓の大きさ、判定を始める最小件数、開いたままにする時間、半開状態で通す試行数。どのライブラリでも名前が違うだけで、この5つに行き着きます。

既定値はそのまま使えません。Resilience4jは最小件数が100件、Pollyも100件で、秒間数件のAPIでは窓が埋まる前に障害が終わります。逆にopossumは最小件数が0件なので、起動直後の1件の失敗で開きます。

置き場所はアプリ内のライブラリが基本です。IstioのDestinationRuleは接続数の上限と外れ値の切り離しを担い、失敗時に何を返すかまでは決めません。Amazon ECSの「デプロイサーキットブレーカー」は名前が同じだけの別機能で、失敗したデプロイを巻き戻す仕組みです。

circuit breakerの状態遷移をライブラリの設定項目へ対応させる手順

Martin Fowlerがbliki「CircuitBreaker」で示した3状態は、各ライブラリにほぼそのまま実装されています。違いが出るのは、閾値の数え方と特殊な状態の有無です。

Closed・Open・Half-Openに加わる強制状態とライブラリごとの呼び名

Closedは通常の状態で、呼び出しを通しながら失敗を数えます。失敗率が閾値を超えるとOpenへ移り、呼び出しを即座に拒否。待機時間が過ぎるとHalf-Openへ移り、決められた数だけ試しに通して、成功すればClosedへ戻ります。失敗すればまたOpenです。

運用で効いてくるのは3状態以外の状態です。Resilience4jのCircuitBreakerドキュメントは、常に拒否する FORCED_OPEN、常に通してメトリクスも取らない DISABLED、常に通しつつメトリクスだけ取る METRICS_ONLY を定義しています。METRICS_ONLY は本番投入の初週に使えます。遮断はせずに失敗率だけを記録し、閾値を決める材料を集められるからです。

Pollyは ManualControl で手動の隔離ができ、その間は IsolatedCircuitException を投げます。gobreakerとopossumには手動の強制状態がなく、必要ならアプリ側でフラグを持ちます。

4ライブラリの既定値比較表と、本番投入前に必ず書き換える既定値

各公式ドキュメントに記載された既定値を並べます(2026年9月29日時点)。

設定 Resilience4j 2系 Polly 8系 gobreaker v2 opossum
開く条件 失敗率50% 失敗率10% 連続失敗が5回超 失敗率50%
判定の窓 直近100件 直近30秒 Interval(0なら消去なし) 直近10秒
最小件数 100件 100件 なし 0件
開いている時間 60秒 5秒 60秒 30秒
半開で通す数 10件 1件 1件(MaxRequests=0) 1件
遅延を失敗に数える 60秒超を遅延扱い 別途Timeout戦略 なし 10秒でタイムアウト

書き換える優先順位は、最小件数、遅延の扱い、開いている時間の順です。最小件数はトラフィックに合わせないと、Resilience4jとPollyでは遮断が起きず、opossumでは起きすぎます。遅延の扱いは、Resilience4jの slowCallDurationThreshold が既定で60秒と長く、呼び出し元のタイムアウトより先に遅延判定が働くことはまずありません。

gobreakerの Interval には注意が要ります。gobreakerのREADMEによれば、0以下のときClosed状態でカウントを消去しません。何日も前の失敗が残り続けるので、10秒前後を明示してください。

Resilience4j 2.4とSpring Bootでブレーカーを設定する手順

Javaで新規に組むならResilience4jが第一候補です。GitHubのv2.4.0リリースノートによれば2026年3月14日(UTC)に公開され、Spring Boot 4とSpring Cloud 5への対応が加わりました。以下はSpring Boot 3系での例です。

依存関係とapplication.ymlで閾値・待機時間を宣言する設定例

依存関係は Resilience4jのSpring Boot 3向けGetting Startedのとおり、本体に加えてactuatorとaopを入れます。設定は呼び出し先ごとのインスタンス名で分けます。

// build.gradle
dependencies {
    implementation "io.github.resilience4j:resilience4j-spring-boot3:2.4.0"
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "org.springframework.boot:spring-boot-starter-aop"
}

# application.yml
resilience4j:
  circuitbreaker:
    instances:
      paymentApi:
        slidingWindowType: TIME_BASED      # 直近N秒で判定する
        slidingWindowSize: 10              # TIME_BASEDでは秒数
        minimumNumberOfCalls: 20           # 20件たまるまで判定しない
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s      # 2秒超を遅延として数える
        slowCallRateThreshold: 80
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 5
        automaticTransitionFromOpenToHalfOpenEnabled: true
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
        ignoreExceptions:
          - com.example.payment.InvalidRequestException
        registerHealthIndicator: true

ignoreExceptions には、呼び出し先の障害ではない失敗を入れます。入力不正のような4xx相当の例外を数えると、利用者の誤操作だけでブレーカーが開きます。v2.4.0では ignoreExceptions が recordExceptions より優先されるよう修正されたので、両方に同じ親クラスが絡む場合も、除外が意図どおりに働くようになった版です。

@CircuitBreakerのfallbackMethodとアスペクト順序の落とし穴

アノテーションを付けたメソッドが、設定したインスタンス名で守られます。

@Service
public class PaymentClient {

    private final RestClient restClient;

    public PaymentClient(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("https://payment.example.com").build();
    }

    @CircuitBreaker(name = "paymentApi", fallbackMethod = "whenOpen")
    public PaymentStatus getStatus(String orderId) {
        return restClient.get()
                .uri("/v1/payments/{id}", orderId)
                .retrieve()
                .body(PaymentStatus.class);
    }

    // 遮断中(CallNotPermittedException)のときだけ「確認中」を返す
    private PaymentStatus whenOpen(String orderId, CallNotPermittedException e) {
        return PaymentStatus.pending(orderId);
    }
}

fallbackの引数の例外型を CallNotPermittedException に絞ると、差し替えるのは遮断中の応答だけです。ここを Exception にすると、通常の失敗まで「確認中」に化けて、障害の発生に呼び出し元が気づけなくなります。

落とし穴はアスペクトの順序です。Getting Startedによれば既定は Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) ) で、Retryが一番外側です。そのためリトライ設定で CallNotPermittedException を再試行対象から外さないと、遮断中の拒否をリトライが繰り返します。再試行の側の設計はリトライパターンの指数バックオフと上限設計が詳しい。

C#のPolly 8・Goのgobreaker・Node.jsのopossumで書く実装例

残りの3言語も、決める値は同じ5つです。コードは各公式ドキュメントの書き方に揃えています。

Polly 8のAddCircuitBreakerで失敗率と遮断時間を指定する例

NuGetのPolly.Coreは2026年9月29日時点で8.8.0が最新です。8系では ResiliencePipelineBuilder に戦略を積む書き方に統一されました。オプション名と既定値はPollyのcircuit breaker戦略のドキュメントに載っています。

using Polly;
using Polly.CircuitBreaker;

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,                          // 既定は0.1
        SamplingDuration = TimeSpan.FromSeconds(10),
        MinimumThroughput = 20,                      // 既定は100
        BreakDuration = TimeSpan.FromSeconds(30),
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => (int)r.StatusCode >= 500)
    })
    .Build();

try
{
    var res = await pipeline.ExecuteAsync(
        async ct => await http.GetAsync("https://payment.example.com/v1/status", ct),
        cancellationToken);
}
catch (BrokenCircuitException)
{
    // 遮断中は呼び出し先へ送らずにここへ来る
}

Pollyの既定の FailureRatio は0.1で、他の3ライブラリの50%より厳しい値です。10件に1件の失敗で開くので、外部APIの呼び出しでは0.3〜0.5に上げるのが無難です。パイプラインは呼び出しのたびに作らず、DIでシングルトンとして持ちます。

gobreaker v2のReadyToTripを連続失敗から失敗率判定へ変える例

gobreakerは2026年1月1日公開のv2.4.0が最新で(Go module proxyで確認)、v2ではジェネリクスで戻り値の型を指定します。既定の ReadyToTrip は連続失敗が5回を超えたら開く判定なので、失敗率で判定したい場合は関数を差し替えます。

package main

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/sony/gobreaker/v2"
)

var cb = gobreaker.NewCircuitBreaker[[]byte](gobreaker.Settings{
	Name:        "payment-api",
	MaxRequests: 5,                // 半開で通す数
	Interval:    10 * time.Second, // 0だとカウントが消えない
	Timeout:     30 * time.Second, // 開いている時間
	ReadyToTrip: func(c gobreaker.Counts) bool {
		// 20件以上たまり、かつ失敗率50%以上で開く
		return c.Requests >= 20 && float64(c.TotalFailures)/float64(c.Requests) >= 0.5
	},
	OnStateChange: func(name string, from, to gobreaker.State) {
		fmt.Printf("%s: %s -> %s\n", name, from, to)
	},
})

func fetch(url string) ([]byte, error) {
	return cb.Execute(func() ([]byte, error) {
		resp, err := http.Get(url)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()
		if resp.StatusCode >= 500 {
			return nil, fmt.Errorf("upstream status %d", resp.StatusCode)
		}
		return io.ReadAll(resp.Body)
	})
}

func main() {
	_, err := fetch("https://payment.example.com/v1/status")
	if errors.Is(err, gobreaker.ErrOpenState) {
		fmt.Println("遮断中のため呼び出していません")
	}
}

http.Get は5xxを返してもerrorにしません。ステータスを見て自分でerrorに変えないと、呼び出し先が500を返し続けてもgobreakerは成功として数えます。Goで最も起きやすい取りこぼしがここです。

opossum 10のfire・fallback・イベント購読で遮断を可視化する例

npmのopossumは2026年9月29日時点で10.0.0が最新で、engines はNode.js 22・24・26です。Node.js 22以降は fetch が組み込みなので、追加の依存なしで書けます。オプションはopossumのREADMEのとおりです。

const CircuitBreaker = require('opossum');

async function getStatus(orderId) {
  const res = await fetch(`https://payment.example.com/v1/payments/${orderId}`);
  if (res.status >= 500) throw new Error(`upstream status ${res.status}`);
  return res.json();
}

const breaker = new CircuitBreaker(getStatus, {
  timeout: 3000,                 // 3秒で失敗扱い(既定は10秒)
  errorThresholdPercentage: 50,
  resetTimeout: 30000,           // 30秒後に半開へ
  rollingCountTimeout: 10000,    // 直近10秒で判定
  volumeThreshold: 20,           // 既定0のままだと1件目の失敗で開く
});

breaker.fallback((orderId) => ({ orderId, status: 'pending' }));
breaker.on('open', () => console.warn('payment-api: open'));
breaker.on('halfOpen', () => console.info('payment-api: halfOpen'));
breaker.on('close', () => console.info('payment-api: close'));

breaker.fire('A-1001').then(console.log).catch(console.error);

opossumの volumeThreshold は既定0です。デプロイ直後にたまたま1件失敗すると失敗率100%と判定され、30秒間すべてを拒否します。open・halfOpen・close のイベントはログやメトリクスへ必ず流してください。遮断が起きたことを後から追えないと、閾値の調整ができません。

アプリ内のブレーカーとIstio・ECSのcircuit breakerを使い分ける基準

「circuit breaker」という名前の設定は、アプリの外にもあります。役割が違うので、どれか1つで全部を賄おうとしないことです。

IstioのDestinationRuleで接続数と外れ値検知を設定するYAML例

Istioでは DestinationRule の connectionPool と outlierDetection がcircuit breakerにあたります。Istio 1.31のCircuit Breakingタスクは動作を見せるため最大接続数1・5xx連続1回で切り離す設定にしており、fortioで同時3接続を送ると63.3%が503になると示しています。本番ではこの値を緩めて使うのが前提です。

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: payment-api
spec:
  host: payment-api.default.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 10
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

Istioが得意なのは、複数のPodのうち壊れた1台だけを負荷分散から外すことです。呼び出し先サービス全体が落ちたときに「確認中」を返すような判断は、アプリ内のブレーカーにしかできません。両方を入れる場合は、Istioを台単位の切り離し、アプリを機能単位の縮退と役割を分けます。

ECSデプロイサーキットブレーカーは別物という前提と閾値の計算式

Amazon ECSのデプロイサーキットブレーカーは、呼び出しの遮断ではなく、失敗したデプロイの検知と巻き戻しの機能です。ECSデベロッパーガイドの該当ページによれば、ローリングアップデート(ECS デプロイコントローラー)でのみ使えます。

aws ecs update-service \
  --cluster my-cluster \
  --service payment-api \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}"

失敗の閾値は既定の BOUNDED_PERCENT で「50% × 必要タスク数」を切り上げ、最小3・最大200に丸めます。必要タスク数1なら3、25なら13、400なら200です。thresholdConfiguration で固定件数の COUNT や上限なしの UNBOUNDED_PERCENT も選択可能。判定にはELBやコンテナのヘルスチェックの結果も使われるため、LivenessとReadinessを分けたヘルスチェックの閾値設計が甘いと、正常なデプロイまで巻き戻されます。ECSのローリング設定全体はローリングアップデートの設定値をまとめた記事にあります。

閾値をトラフィック量から逆算する手順と、ブレーカーが効かない実装ミス4件

ここは判断を言い切ります。閾値は勘で決めず、秒間リクエスト数から計算で決めます。

minimumNumberOfCallsと窓の大きさを秒間リクエスト数から決める計算

秒間5件の決済APIを例に取ります。10秒の時間窓なら、窓の中には常に約50件。最小件数を20件にすると、呼び出し先が完全に落ちてから約4秒で判定が始まります。直前まで成功していた50件が窓から少しずつ抜けていくので、失敗率50%を超えるのは落ちてから約5秒後です。

同じAPIに既定値(件数窓100件・最小100件)を当てると、判定開始まで20秒かかります。秒間0.5件の社内APIなら200秒で、障害の多くはその前に終わるか、呼び出し元のスレッドを食い尽くしています。

  1. ピーク時と閑散時の秒間リクエスト数を、アクセスログから出す
  2. 呼び出し元が遅延に耐えられる秒数(例:5秒)を決める
  3. 閑散時の秒間件数 × その秒数 の半分程度を最小件数にする
  4. 時間窓は耐えられる秒数の2倍を目安にする
  5. 本番の最初の1〜2週間は METRICS_ONLY 相当で失敗率の分布を記録し、閾値を確定する

閑散時に最小件数へ届かない呼び出しは、件数でなく連続失敗で判定したほうが確実です。gobreakerの既定の ReadyToTrip がこの形で、秒間1件未満の管理系APIではむしろこちらが向いています。

ブレーカーを入れても連鎖障害が止まらない実装ミス4件とレビュー観点

レビューで見るのは次の4つです。影響の大きい順に並べました。

  • タイムアウトが無い:失敗が返ってこない限りブレーカーは数えられない。応答待ちで固まる障害では、先にタイムアウトを入れる
  • インスタンスを呼び出しごとに作る:状態が毎回リセットされ、永遠に開かない。PollyのパイプラインやgobreakerはDIやパッケージ変数で共有する
  • 4xxを失敗に数える:利用者の入力ミスで開く。例外の除外設定かステータス判定で、呼び出し先の障害だけを数える
  • fallbackが同じ呼び出し先を叩く:遮断中に別経路で同じサービスへ負荷をかける。fallbackはキャッシュか固定値にとどめる

スレッドやコネクションの枯渇まで防ぎたいなら、ブレーカー単体では足りません。呼び出し先ごとに同時実行数を分けるバルクヘッドパターンの隔離の単位と実装と組み合わせます。

自前のブレーカー実装を見送り、メッシュや再試行だけで済ませる構成の条件

次のどれかに当たるなら、アプリ内のブレーカーは入れません。1つ目は、呼び出しがキュー経由の非同期処理で、失敗はキューに戻して後で再処理すれば済む場合。遮断で守るべき待ち行列がそもそもありません。2つ目は、呼び出し先が1つで、落ちたら画面全体がエラーになる構成。fallbackで返せる代わりの値が無いなら、遮断しても利用者から見た結果は同じです。

3つ目は、Istioなどのサービスメッシュが入っていて、呼び出し先が複数台の同質なPodである場合です。壊れた台の切り離しは outlierDetection で足り、アプリ側は短いタイムアウトと1回の再試行で十分です。外部の決済・配送・認証APIを複数つなぐ構成では逆に、4ライブラリのどれかを必ず入れてください。どこまでアプリで持ちどこをインフラに任せるかは連携先の数と仕様で決まるので、API開発・システム連携の相談窓口で構成ごと検討できます。

circuit breakerの実装・設定・テストに関するよくある質問

実装の段階で出てくる質問を5つ取り上げます。

circuit breakerとサーキットブレーカーパターンは同じものですか?

ソフトウェア開発の文脈では同じものを指します。英語表記のcircuit breakerはライブラリ名や設定キーで使われ、日本語の記事ではサーキットブレーカーパターンと呼ばれることが多いという違いだけです。ただしAmazon ECSの「デプロイサーキットブレーカー」は、呼び出しを遮断するパターンではなく、失敗したデプロイを巻き戻す機能です。電気の遮断器や株式市場の売買停止も同じ英語で呼ばれるため、検索では「resilience4j」「polly」などライブラリ名を足すと目的の情報に早く届きます。

Hystrixで書かれた既存コードはResilience4jへ移行すべきですか?

移行してください。Netflix Hystrixは新機能の開発を止めており、Spring Cloud Circuit BreakerもResilience4jを実装の選択肢にしています。移行は1対1の置き換えになりません。Hystrixはスレッドプールで隔離する方式が中心でしたが、Resilience4jではブレーカーと隔離(Bulkhead)が別の部品です。まず @HystrixCommand を @CircuitBreaker と @TimeLimiter に分け、スレッドプールが必要な呼び出しだけに ThreadPoolBulkhead を足す順で進めると、差分が小さく済みます。

ブレーカーが開いたことをテストで確かめるにはどうすればよいですか?

単体テストでは、呼び出し先をモックにして最小件数ぶん失敗させ、次の呼び出しで遮断の例外(Resilience4jなら CallNotPermittedException、Pollyなら BrokenCircuitException)が出ることを確かめます。待機時間を設定で数百ミリ秒に縮めれば、半開から閉じるまでの検証も数秒で終わる。結合環境では、呼び出し先に遅延や5xxを注入して、ダッシュボードに状態遷移が出るかまで確認してください。注入の手順はカオステストの実験設計とツール選定が参考になります。

複数のPodやインスタンスでブレーカーの状態を共有すべきですか?

原則として共有しません。4ライブラリはいずれもプロセス内のメモリで状態を持ち、Podごとに独立して判定する作りです。Podごとに判定すれば、ネットワーク経路の一部だけが壊れた場合でも、影響を受けたPodだけが遮断できます。Redisなどで共有すると、状態の読み書き自体が新しい障害点になり、共有ストアが落ちたときの挙動まで設計する羽目になります。全Podで一斉に止めたい場合は、ブレーカーの共有ではなく、機能フラグで呼び出しを止めるほうが簡単です。

リトライとcircuit breakerはどちらを外側に置けばよいですか?

Resilience4jのSpring Boot連携は既定でRetryを外側に置き、各試行がブレーカーを通る順序にしています。この順序なら、リトライの1回ずつが失敗として数えられ、遮断が早く効きます。その場合、リトライの対象から遮断の例外を外す設定が必須です。逆にブレーカーを外側にすると、リトライで吸収された失敗は数えられず、遮断は遅れます。どちらを選んでも、再試行の回数と待機時間の上限は別途置いてください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.09.28 テックブログ タイムズカーの不正アクセスと約660万件の流出|免許証画像を退会者まで残さない保管設計
  2. 2026.09.25 コラム 最低賃金引き上げ【令和8年度】47都道府県の改定額・発効日と企業の対応手順
  3. 2026.09.25 コラム 障害者雇用の助成金一覧:月いくら・支給要件と申請書類を勤怠データで揃える方法
  4. 2026.04.03 テックブログ マイナビ情報漏洩11万件|不正アクセスの経緯・対象確認と「登録は危険か」の判断材料
  5. 2026.09.05 コラム 犯罪収益移転防止法の本人確認:2027年4月の対面IC読み取り義務化と改修要件

RELATED POSTS 関連記事

目次