サーキットブレーカーパターンとは?Closed/Open/Half-Openの仕組みをシミュレーションで徹底解説

Martin FowlerとMichael Nygardの一次資料およびResilience4j公式ドキュメントに基づきサーキットブレーカーの3状態を定式化し、自作の離散事象シミュレーションで障害注入時のレイテンシと依存サービスへの到達数を実測して、ブレーカーの効果を定量的に検証する。

1. 障害の連鎖(cascading failure)はなぜ起きるか

マイクロサービス構成では、1つのリクエストを処理するためにサービスA→サービスB→サービスCのように複数の依存先を呼び出すことが多い。この構成における最も厄介な現象の1つが 障害の連鎖(cascading failure) である。あるサービス(例: サービスC)が過負荷や障害でタイムアウトし始めると、それを呼び出しているサービスBのスレッドやコネクションが、Cからの応答(またはタイムアウト)を待つ間、長時間ブロックされる。Bのスレッドプールやコネクションプールが枯渇すれば、Cとは無関係な別のリクエストまで処理できなくなり、Bを呼んでいるAも同様に巻き込まれる。1箇所の障害が呼び出し階層全体に伝播していくのが、障害の連鎖の典型的な構造である。

このメカニズムは、https://yuhi-sa.github.io/posts/20260716_db_pool_queueing/1/で扱ったリトルの法則 \(L = \lambda W\) を使うと定量的に見積もれる。ある依存サービスへの呼び出しが平常時は平均 \(W=20\text{ms}\) で応答し、到着率が \(\lambda=50\text{ req/s}\) だとすると、このサービス呼び出しのために同時にブロックされているスレッド(またはコネクション)数の平均は

\[ L = \lambda W = 50 \times 0.020 = 1 \text{ 本} \]

程度で済む。ところが依存サービスが障害に陥り、応答が来ないままタイムアウト値の \(W=2000\text{ms}\) まで律儀に待つようになると、

\[ L = \lambda W = 50 \times 2.000 = 100 \text{ 本} \]

と、同時にブロックされるスレッド数が100倍に跳ね上がる。呼び出し元のスレッドプールのサイズが200程度しかなければ、この1つの依存先だけでプールの半分が食いつぶされ、他のエンドポイントの処理能力まで道連れにされる。障害の連鎖の本質は「タイムアウトという安全装置そのものが、依存先が完全に止まっている間は逆に資源を延々と拘束する足かせになる」という逆説にある。

Michael Nygardは著書『Release It!』の中で、この種の障害伝播を防ぐパターンとして サーキットブレーカー(Circuit Breaker) を提案した1。この名称は電気回路の遮断器そのものの比喩で、下流の障害を検知したら、それ以上依存サービスへ電流(リクエスト)を流さないようにして、呼び出し元のリソースと、障害中の依存サービス自身の双方を守る、というアイデアである。Martin Fowlerはこのパターンをbliki記事にまとめ、「保護対象の呼び出しを失敗が続くかどうか監視し続けるラッパーで包み、失敗が閾値を超えたら以降の呼び出しをすぐにエラーにして本体を呼ばせないようにする」という要点と、3つの状態(Closed/Open/Half-Open)による定式化を広めた2

2. サーキットブレーカーの3状態

サーキットブレーカーは次の3状態を持つ状態機械としてモデル化できる。

  • Closed(閉): 通常状態。すべてのリクエストを依存サービスへそのまま通す。同時に、直近の呼び出し結果(成功/失敗)をスライディングウィンドウで記録し続ける。ウィンドウ内の失敗率が閾値を超えると、Openへ遷移する。
  • Open(開): 遮断状態。依存サービスへは一切リクエストを送らず、即座に(タイムアウトを待たずに)エラーを返す。一定時間(オープン滞留時間)が経過すると、Half-Openへ遷移する。
  • Half-Open(半開): 試験運転状態。少数のリクエストだけを試験的に依存サービスへ通し、その結果を見る。試験呼び出しの失敗率が閾値未満であれば依存サービスが回復したとみなしClosedへ戻り、閾値以上であればまだ障害中とみなし再びOpenへ戻る。

この3状態の遷移を図示したのが次の図である。

サーキットブレーカーの3状態遷移図。CLOSEDからOPENへはfailureRateThreshold以上の失敗率で遷移し、OPENからHALF_OPENへはwaitDurationInOpenState経過後に遷移し、HALF_OPENでは試行callの失敗率に応じてCLOSEDに戻るかOPENに戻るかが決まる

ポイントは、Openになった瞬間から依存サービスへの呼び出しがゼロになることである。呼び出し元は「タイムアウトが解決するまで待つ」のではなく「Closedのときに蓄積した情報をもとに、呼んでも無駄だと判断してすぐに諦める」ことができる。これによって、1節で述べたスレッド・コネクションの拘束が解消され、依存サービス側も無意味なリクエストの奔流から解放されて回復に専念できる。この「依存サービス側の回復を助ける」という効果は、後述の実験(4節)で定量的に確認する。

3. 主要パラメータの意味

サーキットブレーカーの実装として広く使われているJavaライブラリ Resilience4j の公式ドキュメントは、この状態機械を制御するパラメータの名前と意味を具体的に定義している3。以下は同ドキュメントに基づく主要パラメータである。

パラメータ名デフォルト値意味
failureRateThreshold50%この失敗率以上でClosed→Openに遷移する閾値
slidingWindowTypeCOUNT_BASEDスライディングウィンドウの種類。呼び出し件数ベース(COUNT_BASED)か経過秒数ベース(TIME_BASED)
slidingWindowSize100ウィンドウのサイズ(件数 or 秒数)
minimumNumberOfCalls100失敗率を計算するために必要な最低呼び出し件数。これに満たないうちは判定しない
waitDurationInOpenState60000 msOpenにとどまる時間。経過後にHalf-Openへの遷移判定が行われる
permittedNumberOfCallsInHalfOpenState10Half-Openで試験的に許可する呼び出し件数
automaticTransitionFromOpenToHalfOpenEnabledfalsefalseの場合、バックグラウンドタイマーではなく次の呼び出し試行時にOpen滞留時間の経過を確認する(遅延評価)

slidingWindowSizeminimumNumberOfCallsはいずれもデフォルトが100件と大きめだが、これは本番運用でノイズ(たまたま数件失敗しただけ)に反応しすぎないようにするためのバランスである。逆に言えば、これらを小さくするほどブレーカーの反応は速くなるが、少数の偶発的失敗にも過敏に反応しやすくなる、というトレードオフがある。waitDurationInOpenStateも同様で、短すぎると障害復旧前に何度もHalf-Openへ突入して依存サービスをまた叩いてしまい、長すぎると回復後もしばらく遮断されたままになる。

automaticTransitionFromOpenToHalfOpenEnabledがデフォルトでfalseになっている点は見落とされがちである。これは「Open滞留時間が経過した瞬間に自動でHalf-Openに切り替わる」のではなく、「次にリクエストが来たときに、Open滞留時間が経過していればその場でHalf-Openに切り替えて試験呼び出しとして扱う」という遅延評価であることを意味する。バックグラウンドタイマーを持たないぶん実装がシンプルで、リクエストが来ない限り無駄な試験呼び出しも発生しない設計になっている。4節の自作実装もこの挙動を再現している。

4. 実験: 障害注入でブレーカーあり/なしを比較

以上の理論を裏付けるため、Pythonの標準ライブラリ(heapq)だけを使った離散事象シミュレーション(DES)を自作し、ブレーカーあり/なしで実際に何が変わるのかを実測する。

4.1 モデル設定

  • クライアント群→依存サービス: クライアントはポアソン過程(到着率 \(\lambda=50\text{ req/s}\) )で依存サービスを呼び出す。
  • 依存サービスの挙動: 平常時は平均20ms(指数分布)で応答して成功する。ただし時刻 \(T_1=15\text{s}\) から \(T_2=35\text{s}\) の間だけ「障害」状態になり、その間に依存サービスへ実際に送られた呼び出しはすべて2000msのタイムアウトの末にエラーを返す。
  • 比較対象: (1) ブレーカーなし(常に依存サービスへ直接呼び出す)、(2) ブレーカーあり(3節のパラメータ名に沿った自作の状態機械を経由する)。
  • ブレーカー設定: failureRateThreshold=50%, slidingWindowType=COUNT_BASED, slidingWindowSize=20, minimumNumberOfCalls=20, waitDurationInOpenState=5秒, permittedNumberOfCallsInHalfOpenState=5。デフォルト値(ウィンドウ100件・待機60秒)は本番向けの保守的な値なので、60秒間のシミュレーションで状態遷移の全体像が見えるよう、デモ用に小さめに調整している。
  • 乱数シード: 42に固定。

自作したサーキットブレーカーの状態機械は次の通りである。

class CircuitBreaker:
    """Resilience4jのCircuitBreakerを模した最小実装(CLOSED/OPEN/HALF_OPENの3状態)。"""

    def __init__(self, failure_rate_threshold=0.5, sliding_window_size=20,
                 minimum_number_of_calls=20, wait_duration_in_open_state=5.0,
                 permitted_number_of_calls_in_half_open_state=5):
        self.failure_rate_threshold = failure_rate_threshold
        self.sliding_window_size = sliding_window_size
        self.minimum_number_of_calls = minimum_number_of_calls
        self.wait_duration_in_open_state = wait_duration_in_open_state
        self.permitted_number_of_calls_in_half_open_state = permitted_number_of_calls_in_half_open_state

        self.state = "CLOSED"
        self.closed_window = deque(maxlen=sliding_window_size)  # 1=成功, 0=失敗
        self.half_open_results = []
        self.half_open_admitted = 0
        self.opened_at = None

    def try_acquire_permission(self, now):
        """呼び出しを許可するか判定する。(True, tag) or (False, None) を返す。"""
        if self.state == "OPEN":
            # automaticTransitionFromOpenToHalfOpenEnabled=false相当:
            # 次の呼び出し試行時にOpen滞留時間の経過を確認する遅延評価。
            if now - self.opened_at >= self.wait_duration_in_open_state:
                self._transition_to_half_open(now)
            else:
                return False, None

        if self.state == "HALF_OPEN":
            if self.half_open_admitted < self.permitted_number_of_calls_in_half_open_state:
                self.half_open_admitted += 1
                return True, "HALF_OPEN"
            return False, None

        return True, "CLOSED"  # CLOSED

    def on_result(self, now, success, tag):
        if tag == "CLOSED":
            if self.state != "CLOSED":
                return
            self.closed_window.append(1 if success else 0)
            if len(self.closed_window) >= self.minimum_number_of_calls:
                failure_rate = 1 - sum(self.closed_window) / len(self.closed_window)
                if failure_rate >= self.failure_rate_threshold:
                    self._transition_to_open(now)
        elif tag == "HALF_OPEN":
            if self.state != "HALF_OPEN":
                return
            self.half_open_results.append(1 if success else 0)
            if len(self.half_open_results) >= self.permitted_number_of_calls_in_half_open_state:
                failure_rate = 1 - sum(self.half_open_results) / len(self.half_open_results)
                if failure_rate >= self.failure_rate_threshold:
                    self._transition_to_open(now)
                else:
                    self._transition_to_closed(now)

    def _transition_to_open(self, now):
        self.state = "OPEN"
        self.opened_at = now
        self.half_open_results = []
        self.half_open_admitted = 0

    def _transition_to_half_open(self, now):
        self.state = "HALF_OPEN"
        self.half_open_results = []
        self.half_open_admitted = 0

    def _transition_to_closed(self, now):
        self.state = "CLOSED"
        self.closed_window.clear()

到着イベントでは、ブレーカーに try_acquire_permission で許可を求め、許可された場合のみ依存サービスへの呼び出しイベントを予約する。呼び出しが完了するとその結果(成功/失敗)を on_result でウィンドウに記録し、閾値を超えていれば状態を遷移させる。tagには許可された時点の状態(CLOSEDHALF_OPEN)を持たせ、完了時にすでに状態が変わっていれば(タイムアウトの長い呼び出しが完了する頃には別の状態に移っていることがある)そのウィンドウへの反映をスキップすることで、状態遷移とタイミングの整合性を保っている。

4.2 実測結果: レイテンシの時系列

障害期間中(15〜35秒)の集計は次の通りである。

指標ブレーカーなしブレーカーあり
障害期間中の総リクエスト数987件987件
うち依存サービスへ到達した件数987件(100.00%)102件(10.33%)
障害期間中の平均レイテンシ2000.00 ms207.14 ms
障害期間中のp99レイテンシ2000.00 ms2000.00 ms

1秒ビンごとの平均・p99レイテンシの時系列をプロットしたのが次の図である。

クライアントから見た1秒ビン平均・p99レイテンシの時系列。ブレーカーなし(オレンジ)は障害期間中ずっと平均・p99ともに2000ms付近に張り付くのに対し、ブレーカーあり(青)は障害発生から約2秒後に0.5ms程度まで急落し、Half-Open試行のタイミングでのみ一時的に2000ms付近まで跳ね上がる

ブレーカーなしでは、障害期間中ずっと平均・p99レイテンシが2000ms(タイムアウト値そのもの)に張り付く。全リクエストが例外なく依存サービスの応答(またはタイムアウト)を待たされるので当然の結果だが、これが1節で述べたスレッド拘束の直接的な原因になる。

一方ブレーカーありでは、障害発生(t=15s)から約2.17秒後(t=17.17s)にブレーカーがOpenへ遷移し、そこから平均レイテンシは0.5ms程度まで急落する。これは、Openになった呼び出しが依存サービスへ届く前に即座に失敗として返されるためである。ただし興味深いのは、p99レイテンシは障害期間中もときどき2000ms付近まで跳ね上がっている点である。これはHalf-Openへの遷移タイミング(t≈22.17s, 29.22s)で、少数の試験呼び出しが実際に依存サービスへ送られ、まだ障害が続いているためその試験呼び出し自体がタイムアウトの2000msを要するからである。つまりブレーカーは平均レイテンシを劇的に改善するが、Half-Open中の試験呼び出しに限っては、そのものがタイムアウト時間分のレイテンシを引き受ける、という非自明な事実が実測から確認できる。

4.3 実測結果: 依存サービスへの到達リクエスト数

もう一つの重要な指標が、実際に依存サービスへ到達したリクエスト数である。ブレーカーが機能していれば、障害中は依存サービスへの負荷そのものが激減するはずである。

依存サービスに実際に到達したリクエスト数の1秒ビン時系列。ブレーカーなし(オレンジ)は障害期間中も1秒あたり40〜60件が途切れず依存サービスに到達し続けるのに対し、ブレーカーあり(青)は障害発生直後にほぼ0件まで落ち込み、Half-Open試行のタイミングでのみ数件のスパイクが生じる

障害期間中に依存サービスへ到達した件数を集計すると、ブレーカーなしが987件(到達率100%)であるのに対し、ブレーカーありは102件(到達率10.33%)にとどまった。実に885件、割合にして約90%のリクエストが依存サービスに到達する前に遮断されている。この102件の内訳は、ブレーカーがOpenに遷移するまでに間に合ってしまった初期の呼び出し(約20件)と、その後2回のHalf-Open試行(各5件、うち一部は同時に他のリクエストも紛れ込む)によるものである。障害が続く依存サービスの視点に立てば、「本来なら1秒間に40〜60件叩かれ続けるところを、ほぼゼロまで減らしてもらえる」ことになり、これが「ブレーカーが依存サービスの回復を助ける」という主張の定量的な裏付けになる。

4.4 実測結果: Half-Open経由の自動復帰

最後に、ブレーカーの状態遷移そのものをタイムラインとして可視化する。

ブレーカーの状態遷移タイムライン。0〜17.17秒はCLOSED、17.17秒でOPENに遷移し、22.17秒と29.22秒の2回Half-Openを試みるがいずれも依存サービスがまだ障害中のためOPENに戻り、依存サービスの障害が終わった後の36.25秒の3回目のHalf-Open試行で成功しCLOSEDに復帰する

実測されたイベント列は次の通りである。

t= 0.00s  CLOSED (シミュレーション開始)
t=17.17s  CLOSED -> OPEN       (障害発生から2.17秒後。20件のウィンドウが失敗で埋まった)
t=22.17s  OPEN   -> HALF_OPEN  (waitDurationInOpenState=5秒 経過)
t=24.22s  HALF_OPEN -> OPEN    (試験呼び出し5件、まだ障害中のため全滅し再びOpenへ)
t=29.22s  OPEN   -> HALF_OPEN  (再び5秒経過)
t=31.23s  HALF_OPEN -> OPEN    (2回目の試験も全滅)
t=36.25s  OPEN   -> HALF_OPEN  (3回目、このときには依存サービスは既に復旧済み(t=35s))
t=36.38s  HALF_OPEN -> CLOSED  (試験呼び出しが成功し正常復帰)

依存サービスの障害は \(T_2=35\text{s}\) に終わっているが、ブレーカーが実際にClosedへ復帰したのは \(t=36.38\text{s}\) で、約1.38秒の遅れがある。これは「ちょうど障害が終わったタイミング」を狙ってHalf-Openに入ったわけではなく、たまたま5秒周期の試験タイミング(\(t=31.23\text{s}\) の次)が \(t=36.25\text{s}\) だったためで、依存サービス復旧後に初めて試験呼び出しが送られた瞬間に自動的に検知・復帰できていることが分かる。人手を介さず、依存サービスの状態を能動的にポーリングすることもなく、通常のリクエストの流れに乗せた試験呼び出しだけで自動復旧が完結している点が、このパターンの実用上の価値である。

5. リトライ・タイムアウトとの関係

サーキットブレーカーは単独で機能するものではなく、タイムアウト・リトライと合わせて初めて効果を発揮する。https://yuhi-sa.github.io/posts/20260720_exponential_backoff/1/で扱った指数バックオフ+ジッターとの関係を整理すると、両者は時間スケールの異なる障害に対する役割分担であるといえる。

  • リトライ+バックオフ: ネットワークの瞬断や一時的な過負荷など、数百ミリ秒〜数秒で自然に解消する短期の一時障害を主な対象とする。個々のリクエストの視点で「もう一度試せば成功するかもしれない」という前提が成り立つ状況に向く。
  • サーキットブレーカー: 依存サービスが明確にダウンし続けている、数秒〜数十秒(あるいはそれ以上)続く持続障害を主な対象とする。個々のリクエストをリトライしても無駄であり、リトライそのものが依存サービスへの負荷になってしまう状況で、リトライを含めた呼び出し自体を止める。AWS Builders’ Libraryも、タイムアウト・リトライ・バックオフという3本柱に加えて、クライアント側にトークンバケットを持たせてリトライ量そのものを制限する仕組みを紹介しており、これは広い意味でのサーキットブレーカーの一種として位置づけられている4

指数バックオフの記事で見た通り、ジッターを伴わない指数バックオフは同期を崩せず、大量のクライアントが同時に失敗すると「衝突する塊の大きさ」自体は変わらない。サーキットブレーカーは、この「衝突する塊」を依存サービスに到達させる前段でそもそも遮断してしまうという、リトライ設計とは異なるレイヤーの防御線である。実務では、1回のリクエストに対して「タイムアウトを設定する→失敗したらバックオフ+ジッターで数回リトライする→それでもダメならサーキットブレーカーに判断を委ね、Openなら即座に諦める」という順序で3つの仕組みを重ねて使うのが標準的な設計である。

6. 実装: Resilience4jとPython

6.1 Resilience4j(Java)

Spring Bootと組み合わせる場合、application.ymlでインスタンスごとに設定するのが一般的である。

resilience4j.circuitbreaker:
  instances:
    paymentService:
      failureRateThreshold: 50
      slidingWindowType: COUNT_BASED
      slidingWindowSize: 100
      minimumNumberOfCalls: 100
      waitDurationInOpenState: 60000
      permittedNumberOfCallsInHalfOpenState: 10
      automaticTransitionFromOpenToHalfOpenEnabled: false

コードから直接設定する場合は次のようになる。

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .slidingWindowType(SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(100)
    .minimumNumberOfCalls(100)
    .waitDurationInOpenState(Duration.ofSeconds(60))
    .permittedNumberOfCallsInHalfOpenState(10)
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("paymentService", config);

パラメータ名は本記事の3節・4節で使ったものとそのまま対応しており、本番ではデフォルト値(ウィンドウ100件・待機60秒)から出発し、実際のトラフィック量とSLOに合わせて調整するのが安全である。

6.2 Python(自作クラス)

Pythonのエコシステムにはpybreakerのような既存ライブラリもあるが、4節で自作したCircuitBreakerクラスをそのまま関数呼び出しのラッパーとして使うこともできる。

breaker = CircuitBreaker(
    failure_rate_threshold=0.5,
    sliding_window_size=20,
    minimum_number_of_calls=20,
    wait_duration_in_open_state=5.0,
    permitted_number_of_calls_in_half_open_state=5,
)

def call_with_breaker(now, dependency_call):
    allowed, tag = breaker.try_acquire_permission(now)
    if not allowed:
        raise CircuitOpenError("breaker is open, failing fast")
    try:
        result = dependency_call()
        breaker.on_result(now, success=True, tag=tag)
        return result
    except Exception:
        breaker.on_result(now, success=False, tag=tag)
        raise

実運用ではnowに時刻を渡す代わりにtime.monotonic()を使い、dependency_callに実際のHTTPクライアントやDBドライバの呼び出しを渡せば、そのままアプリケーションコードに組み込める。

7. 運用の注意

実測結果と実装を踏まえ、運用上の注意点を整理する。

  1. 閾値は本番のエラー率のばらつきを踏まえて決める: failureRateThresholdを低く(例: 20%)しすぎると、平常運転でも起こりうる偶発的なエラーの揺らぎでブレーカーが誤ってOpenになりやすくなる。逆に高く(例: 80%)しすぎると、本当に障害が起きてもなかなか検知できない。まずはデフォルトの50%付近から始め、実際のエラー率のヒストグラムを見ながら調整するのが安全である。
  2. フォールバックを事前に設計する: ブレーカーがOpenのとき、呼び出し元は「即座にエラーを返す」以外に何をすべきかを決めておく必要がある。キャッシュされた古いデータを返す、デフォルト値を返す、機能を一時的に無効化して縮退運転するなど、依存サービスなしでも成立する代替パスをあらかじめ用意しておかないと、ブレーカーは単に「速く失敗する」だけの仕組みで終わってしまう。
  3. ブレーカーの状態変化を監視する: Open/Half-Open/Closedの遷移そのものをメトリクスとして可視化し、アラートを設定しておく。ブレーカーが頻繁にOpenとHalf-Openを行き来している場合、依存サービスの障害が長引いているか、waitDurationInOpenStateが短すぎる可能性が高い。4節の図4のような状態タイムラインを本番のダッシュボードでも再現できると、障害発生から復旧までの経緯を後から追いやすい。
  4. ブレーカーの単位を適切に区切る: 1つの巨大な依存先(例: 「DB全体」)に対して1つのブレーカーをかけるのではなく、エンドポイントやテーブル単位など、障害の影響範囲に合わせて粒度を分けることで、無関係な機能まで道連れで遮断してしまう事態を避けられる。

8. まとめ

サーキットブレーカーは、依存サービスの障害という「持続的な悪天候」に対して、クライアント自身がリソースを守りながら、依存サービスの回復も助けるための仕組みである。今回のシミュレーションでは、次の点が定量的に確認できた。

  • 障害発生から約2.17秒後にブレーカーがOpenへ遷移し、それ以降クライアントから見た平均レイテンシは2000msから0.5ms程度へ劇的に改善した。
  • 障害期間中に依存サービスへ到達したリクエストは、ブレーカーなしの987件(100%)に対し、ブレーカーありでは102件(10.33%)にとどまり、約90%の負荷が遮断された。
  • p99レイテンシはHalf-Open試行のタイミングで一時的に2000ms付近まで戻ることがあり、ブレーカーは平均レイテンシは劇的に改善する一方、試験呼び出し自体のタイムアウトは避けられないという非自明な事実も観測された。
  • 依存サービスの復旧(t=35s)からわずか1.38秒後(t=36.38s)に、人手を介さずHalf-Open経由で自動的にClosedへ復帰した。

「失敗したらとりあえず遮断する」という発想自体は単純だが、その効果はこうして実際に手を動かして確認して初めて実感できる。リトライ・バックオフ・タイムアウト・サーキットブレーカーは、それぞれ異なる時間スケールの障害を担当する道具であり、どれか1つで済ませようとせず、組み合わせて設計することが分散システムの耐障害性を支える。

FAQ

Q1. リトライとどう使い分ける? A. リトライ+バックオフは、数百ミリ秒〜数秒で解消する短期の一時障害を想定した仕組みであり、サーキットブレーカーは依存サービスが持続的にダウンしている状況を想定した仕組みである。実務では両方を併用し、個々の呼び出しはバックオフ付きリトライで数回まで試したうえで、ブレーカーがOpenであればリトライ自体を行わずに即座に諦める、という順序で組み合わせる。

Q2. Half-Openで何件試すべき? A. Resilience4jのデフォルトは10件である。件数が少なすぎるとたまたまの成功/失敗で判定がぶれやすく、多すぎると依存サービスがまだ回復し切っていない場合に再び大きな負荷をかけてしまう。本記事の実験では5件で試験し、実際に2回の誤検知(まだ障害中なのに試験してOpenへ戻る)を経て3回目で正しく復帰できており、少なすぎない範囲でトラフィック量に応じて調整するのが実用的である。

Q3. フォールバックがない場合は? A. フォールバックを用意できない場合でも、サーキットブレーカーには「即座に失敗を返すことで、依存サービスへの無駄な負荷とクライアント側のリソース拘束を防ぐ」という効果自体は残る。ただしユーザー体験としては単なるエラー画面になるため、キャッシュされた前回値を返す、機能の一部だけを無効化するといった縮退運転の設計を、優先度の高い依存先から順に検討していくことが望ましい。

Q4. Hystrixはまだ使える? A. Netflixの公式リポジトリでは「Hystrix is no longer in active development, and is currently in maintenance mode.」と明記されており、Hystrixはメンテナンスモードに入っている5。Netflix自身も新規プロジェクトではresilience4jのような活発にメンテナンスされているライブラリの利用を推奨しており、既存のHystrix採用箇所はともかく、新規に導入する理由は乏しい。

関連記事

他の分野の定番書は エンジニアにおすすめの技術書10選 にまとめている。

参考文献


  1. Nygard, M. T. (2018). Release It!: Design and Deploy Production-Ready Software (2nd ed.). Pragmatic Bookshelf. ↩︎

  2. Fowler, M. (2014). CircuitBreaker. martinfowler.com/bliki. https://martinfowler.com/bliki/CircuitBreaker.html  ↩︎

  3. CircuitBreaker. Resilience4j Documentation. https://resilience4j.readme.io/docs/circuitbreaker  ↩︎

  4. Brooker, M. (2019). Timeouts, retries, and backoff with jitter. Amazon Builders’ Library. https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/  ↩︎

  5. Netflix/Hystrix. GitHub. https://github.com/Netflix/Hystrix  ↩︎