アーキテクチャ●●●○○

サーキットブレーカー

Circuit Breakerサーキットブレーカー

障害中の依存先への呼び出しを一時遮断するパターン。失敗の連鎖をブレーカーで断つ。

概要

サーキットブレーカーは、失敗が続いている依存先への呼び出しを一時的に「遮断」する設計パターンです。名前の由来は電気回路のブレーカーで、過負荷を検知したら回路を切って火事(=障害の連鎖)を防ぎ、安全が確認できたら復帰する、という振る舞いをソフトウェアの呼び出しに持ち込んだものです。

遮断中、呼び出し側は相手に接続を試みることすらせず、即座にエラー(あるいは代替の応答)を返します。一見冷たい振る舞いですが、これには二重の効果があります。呼び出す側は返らない応答を待ってスレッドやコネクションを浪費せずに済み、呼び出される側は弱っているところにリクエストを浴びせられずに回復へ専念できる。マイクロサービスのように依存先が多いシステムでは、タイムアウトリトライと指数バックオフと並ぶ安定性パターンの定番として、ほぼ標準装備になっています。

なぜ生まれたか

このパターンを広めたのは、Michael Nygard が2007年の著書『Release It!』で示した安定性パターン群です。背景にあるのは、統合点(他システムとの接続部分)こそがシステム最大の急所だという実務の経験でした。依存先が1つ不調になると、そこへの呼び出しが軒並み遅延し、待ちがスレッドプールを食い潰し、呼び出し元自身が不健全になり、さらにその呼び出し元へ — と障害が連鎖的に伝播します。これがカスケード障害(連鎖障害)です。

障害は「応答待ち」の形で呼び出し元へ遡って伝播するフロントスレッド枯渇サービスA接続が滞留サービスB応答を待ち続ける依存先 X障害中×応答が返らないまま、各層でスレッドと接続が待ちに占有されていく
カスケード障害 — 依存先1つの停止が、応答待ちの滞留となって呼び出し連鎖を遡る

タイムアウトは個々の待ちを打ち切ってくれますが、それだけでは足りません。依存先が完全に沈黙している間も、新しいリクエストは次々に来て、その一つひとつがタイムアウトまでの数秒間、資源を握り続けます。失敗すると分かりきった呼び出しに毎回コストを払い続けるのは無駄なうえに、リトライと組み合わさると弱った相手への追い打ちにもなります。「失敗が続いているなら、しばらく試すのをやめる」という判断を人手ではなく仕組みで行う — それがサーキットブレーカーの発明でした。

詳細

Closed / Open / Half-Open の3状態

サーキットブレーカーは呼び出しの結果を常に記録しながら、3つの状態を遷移します。電気回路と同じく「Closed(閉)=電流が流れる=通信できる」「Open(開)=回路が切れている=遮断」なので、直感と逆に感じる名前に注意してください。

Closed 閉通常どおり呼び出すOpen 開呼び出さず即失敗を返すHalf-Open 半開少数だけ試験的に通す失敗率がしきい値を超える一定時間の経過試験呼び出しが成功試験呼び出しが失敗正常時は Closed に留まり、障害の蓄積で右へ、回復の確認で左へ戻る
サーキットブレーカーの3状態 — 失敗の蓄積で開き、試験的な成功で閉じる

平常時は Closed で、呼び出しはそのまま通過し、ブレーカーは裏で成功・失敗を数えています。失敗率や連続失敗数、あるいは応答の遅延がしきい値を超えると Open に遷移し、以降の呼び出しは依存先に到達させず、その場で即座にエラーを返します(フェイルファスト)。Open のまま一定時間(クールダウン)が経過すると Half-Open に移り、ごく少数のリクエストだけを試験的に通します。それが成功すれば「回復した」とみなして Closed に戻り、失敗すればまた Open に戻ってクールダウンをやり直します。

依存サービスブレーカークライアント依存サービスブレーカークライアント失敗率が閾値超過で Open へ負荷が消えて回復に専念できる一定時間後に Half-Open へClosed に復帰呼び出し転送タイムアウトが連続呼び出し即座に失敗を返す呼び出し試験的に1件だけ転送成功

Half-Open が挟まっているのが設計の妙です。回復したかどうかは実際に呼んでみないと分かりませんが、いきなり全トラフィックを戻すと、回復しかけの相手を再び押し潰しかねません。「偵察を少数だけ送り、無事なら本隊を戻す」という段階的な復帰が、再発の振動を防ぎます。

何をもって「開く」か — 発火条件の設計

ブレーカーを開くトリガーは、単純な連続失敗数よりも、直近の時間窓(またはリクエスト窓)での失敗率で判定するのが一般的です。「直近10秒間で20件以上のリクエストがあり、失敗率が50%を超えたら開く」といった具合で、最低リクエスト数の条件は、閑散時にたまたま数件失敗しただけで開いてしまう誤発火を防ぎます。また、明示的なエラーだけでなく「遅すぎる成功」も失敗に数える実装が多くあります。タイムアウト寸前の応答ばかり返す依存先は、資源占有の観点では落ちているのと大差ないためです。何を失敗と数えるかも重要で、リトライと指数バックオフの記事で述べた分類と同様に、4xx のようなクライアント起因のエラーは相手の健全性とは無関係なので、通常はカウントから除外します。

しきい値やクールダウン時間の調整は、依存先のメトリクスを見ながらの継続的なチューニングになります。敏感すぎれば正常時にも開いて可用性を自ら削り、鈍感すぎれば連鎖障害を止められません。ブレーカーの状態変化はそれ自体が「依存先に異常がある」という強いシグナルなので、状態遷移をメトリクスとして出力し、監視につなぐのが定石です。

開いている間どうするか — フォールバック

ブレーカーが開いたとき、利用者にエラー画面を見せるだけでは芸がありません。多くの実装は「フォールバック」を組み合わせます。キャッシュに残る少し古いデータを返す、デフォルト値や空の結果で画面を成立させる、処理をメッセージキューに積んで後で実行する、といった代替手段です。おすすめ商品の枠が出ないだけでECサイトの購入は続けられる、というように、一部の機能を犠牲にして全体を生かすこの発想はグレースフルデグラデーションそのものであり、ブレーカーはその発動スイッチとして機能します。

実装と実務での位置づけ

かつては Netflix の Hystrix がこのパターンの代名詞で、現在は Resilience4j などのライブラリや、サービスメッシュAPI ゲートウェイの機能としてアプリケーションコードの外側で適用する形も一般的です。実務上の注意点として、ブレーカーの単位は「依存先サービスごと」あるいは「エンドポイントごと」に分けること(1つの重いエンドポイントの障害で、同じサービスの健全な機能まで遮断しないため)、そしてインスタンスごとに独立したブレーカーはそれぞれ別々に統計を取るため、開くタイミングが揃わないことを理解しておくことが挙げられます。また、ブレーカーはあくまで「境界を守る」部品であり、依存先を直すものではありません。遮断で稼いだ時間の間に根本原因へ対処するインシデント対応と組み合わさって、初めて意味を持ちます。同じく境界を守る仲間として、資源を区画ごとに分離するバルクヘッドと併用されることも多く、タイムアウト → リトライ → ブレーカーという系譜の到達点として押さえておきたいパターンです。