この記事の要点
• サーキットブレーカーは障害の連鎖伝播を防ぐためのClosed/Open/Half-Openの3状態遷移パターン
• Fail Fastで上流のリソースを保護し、下流の回復時間を確保する
• Retry・Bulkhead・Timeoutなど他のレジリエンスパターンと組み合わせて使う
サーキットブレーカーパターンとは
サーキットブレーカーパターン(Circuit Breaker Pattern)は、Michael Nygard が著書『Release It!』で広め、Martin Fowler が体系化した分散システムのレジリエンスパターンです。電気回路のブレーカー(漏電遮断器)からその名を取っており、外部サービスの障害が連鎖してシステム全体を巻き込むのを防ぐ仕組みを提供します。
stateDiagram-v2
[*] --> Closed
Closed --> Open: 失敗閾値超過
Open --> HalfOpen: タイムアウト経過
HalfOpen --> Closed: 成功
HalfOpen --> Open: 失敗
なぜサーキットブレーカーが必要か
連鎖障害の恐怖
分散システムでは、あるサービスの障害が依存しているサービスに波及し、最終的にシステム全体を停止させる「カスケード障害(Cascading Failure)」が発生し得ます。たとえば下流のデータベースが応答遅延を起こすと、上流のサービスはリクエストを溜め込み、スレッドプールやコネクションが枯渇し、無関係なリクエストまで失敗するようになります。
タイムアウトだけでは不十分
各リクエストにタイムアウトを設定しても、毎回タイムアウト時間まで待ってから失敗するため、リソース消費は止まりません。秒間 1000 リクエストが 30 秒タイムアウトするシステムでは、瞬時に 30000 件の保留中リクエストが生まれます。
サーキットブレーカーの効果
サーキットブレーカーは「明らかに失敗している依存先」への呼び出しを即座に拒否することで、上流のリソースを守り、下流のサービスにも回復の余裕を与えます。
基本原則
3 つの状態
サーキットブレーカーは 3 つの状態を持ち、外部サービスの健康状態に応じて遷移します。
- Closed(閉): 通常状態。リクエストは透過的に通過する。失敗回数をカウント。
- Open(開): 遮断状態。リクエストは即座に拒否(FailFast)。一定時間後に Half-Open へ。
- Half-Open(半開): 試験運用状態。少数のリクエストを通過させて成否を観察する。
状態遷移の条件
flowchart LR
C["Closed<br/>(通常運用)"]
O["Open<br/>(即時失敗)"]
H["Half-Open<br/>(試行中)"]
C -->|"失敗率>閾値"| O
O -->|"タイムアウト経過"| H
H -->|"試行成功"| C
H -->|"試行失敗"| O
Fail Fast の哲学
ポイント: Closed から Open への遷移により、呼び出し元は「失敗しそうなリクエストに時間を費やさず、すぐに代替手段に進む」ことができます。このFail Fastこそが連鎖障害を断ち切る鍵です。
構成要素の詳細
失敗カウンタ
ある期間内の失敗数(または失敗率)を計測します。固定ウィンドウかスライディングウィンドウかで挙動が変わります。
閾値
「直近 100 件中 50 件失敗したら開く」「直近 60 秒で失敗率 50% を超えたら開く」など、明確なルールを設定します。
Open 状態の保持時間
Open 状態が継続する時間(タイムアウト)を決めます。短すぎると下流が回復する前に再開され、長すぎると不要に拒否し続けます。一般には数秒〜数十秒が出発点です。
Half-Open での試行数
Half-Open では、すべてのリクエストではなく一部のみを通過させます。1 件だけ試して成否を判定する単純な方式と、数件並行に試す方式があります。
フォールバック
サーキットが Open のとき、エラーを返す代わりに代替応答を返す仕組みです。キャッシュされた値、デフォルト値、低品質な代替経路などを使います。
実装例(TypeScript)
最小限のサーキットブレーカー
type State = "CLOSED" | "OPEN" | "HALF_OPEN";
interface BreakerOptions {
failureThreshold: number;
resetTimeoutMs: number;
}
export class CircuitBreaker<T> {
private state: State = "CLOSED";
private failures = 0;
private nextAttempt = 0;
constructor(
private readonly action: () => Promise<T>,
private readonly options: BreakerOptions,
) {}
async call(): Promise<T> {
if (this.state === "OPEN") {
if (Date.now() < this.nextAttempt) {
throw new Error("Circuit breaker is OPEN");
}
this.state = "HALF_OPEN";
}
try {
const result = await this.action();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = "CLOSED";
}
private onFailure(): void {
this.failures += 1;
if (
this.state === "HALF_OPEN" ||
this.failures >= this.options.failureThreshold
) {
this.state = "OPEN";
this.nextAttempt = Date.now() + this.options.resetTimeoutMs;
}
}
}
使用例
import { CircuitBreaker } from "./CircuitBreaker";
async function fetchUser(id: string): Promise<unknown> {
const res = await fetch(`https://users.example.com/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const breaker = new CircuitBreaker(() => fetchUser("123"), {
failureThreshold: 5,
resetTimeoutMs: 10_000,
});
try {
const user = await breaker.call();
console.log(user);
} catch (err) {
console.error("Fallback used:", err);
}
フォールバック付きラッパー
async function withFallback<T>(
breaker: CircuitBreaker<T>,
fallback: () => Promise<T>,
): Promise<T> {
try {
return await breaker.call();
} catch {
return await fallback();
}
}
メリット
- 連鎖障害の防止: 失敗している下流の影響が上流に波及しない
- リソース保護: スレッド・コネクション・メモリの枯渇を防ぐ
- 下流の回復支援: 呼び出しが止まることで下流が復旧しやすい
- 応答時間の改善: タイムアウト待ちをスキップできる
- 観測可能性向上: 状態変化が運用上の重要シグナルになる
デメリット
- 複雑さの増加: 状態管理やパラメータチューニングが必要
- 誤検知のリスク: 一時的な障害で開いてしまうと正常リクエストも拒否
- フォールバック設計の負担: 代替経路を別途用意しなければ意味が薄い
- 分散環境での同期問題: 複数インスタンスで状態を共有するか個別に持つかの設計判断が必要
- テストの難しさ: 障害シナリオを網羅的に再現する必要がある
ユースケース
- マイクロサービス間の同期通信: REST/gRPC 呼び出しの保護
- 外部 API クライアント: 決済、地図、天気などの SaaS 連携
- データベースクライアント: レプリカへの読み取り要求の保護
- メッセージブローカー: 一時的なブローカー障害からの保護
- CDN・キャッシュ層: 元サーバーへのフォールバック制御
注意: サーキットブレーカーの閾値設定を間違えると、一時的な障害で過剰に遮断したり、逆に全く機能しなかったりします。本番トラフィック量に基づいた段階的なチューニングが必要です。
よくある落とし穴
タイムアウトとの不整合
呼び出し側のタイムアウトがサーキットブレーカーの判定より長いと、ブレーカーが動作する前にリソースが枯渇します。両者は整合させる必要があります。
全リクエストを 1 つのブレーカーに集約
1 つのサービスでも API ごとに健康状態は異なります。エンドポイントや操作単位でブレーカーを分けるのが理想です。
失敗の定義が曖昧
HTTP 500 だけを失敗にカウントしてタイムアウトを無視する、といった設定ミスはよくあります。タイムアウト・接続失敗・回線エラーも失敗に含めます。
Half-Open での過剰試行
Half-Open のときに一気に多数のリクエストを流すと、復旧途中の下流を再びダウンさせます。試行数は 1〜数件に絞ります。
フォールバックがない
ブレーカーが開いた瞬間にすべてエラー画面では意味がありません。低品質でも代替を返す設計が必要です。
他パターンとの比較
| パターン | 目的 | 主な動作 | 適用層 |
|---|---|---|---|
| Circuit Breaker | 連鎖障害の防止 | 失敗時に呼び出しを遮断 | クライアント側 |
| Retry | 一時的失敗からの回復 | 失敗時に再試行 | クライアント側 |
| Bulkhead | リソース隔離 | 接続プールを分離 | クライアント/サーバ |
| Timeout | 応答時間の上限保証 | 一定時間で失敗 | 両側 |
| Rate Limiter | 過負荷の防止 | 流量を制限 | サーバ側 |
サーキットブレーカーは Retry と組み合わせて使われることが多いですが、闇雲に Retry すると下流をさらに痛めるため、Retry 回数とブレーカー閾値は協調設計します。
ベストプラクティス
1. 適切なメトリクスを観測
state 遷移、失敗率、フォールバック発動回数を必ずメトリクスに送り、ダッシュボードで可視化します。
2. アラートを設定
ブレーカーが Open になることは下流の異常を示すシグナルです。すぐに気づける運用体制を構築します。
3. パラメータは段階的に調整
最初は緩めの閾値から始め、実トラフィックを観察しながらチューニングします。
実践メモ: Resilience4j(Java)、Polly(.NET)、opossum(Node.js)など実績あるライブラリを使うのが安全です。レースコンディションやエッジケースに既に対処されています。
4. 既存ライブラリを活用
Resilience4j(Java)、Polly(.NET)、opossum(Node.js)など実績あるライブラリを使うのが安全です。
5. 障害テストを実施
カオスエンジニアリングの一環として、意図的に下流を落とし、ブレーカーが期待通り動くか検証します。
6. 操作単位で粒度を分ける
「ユーザー取得」と「注文検索」は別の依存関係です。1 つの巨大ブレーカーに統合せず、適切に分割します。
まとめ
サーキットブレーカーパターンは、分散システムが避けて通れない「依存先の障害」と向き合うための基本装備です。仕組みは単純な状態遷移ですが、パラメータ設計・フォールバック設計・観測体制まで含めて実装すると、システムのレジリエンスが大きく向上します。連鎖障害は実際に起きてから対処するのでは手遅れです。新規開発の段階から組み込み、本番運用とともに磨いていくのがレジリエントなサービスへの近道です。
さらに踏み込んだトピック
スライディングウィンドウ vs 固定ウィンドウ
失敗率を計測する際、固定時間ウィンドウ(過去 60 秒)はシンプルですが境界で挙動が不連続になります。スライディングウィンドウ(直近 N 件)は精度が高い反面、メモリと計算量が増えます。Resilience4j では両方の方式が選択可能で、トラフィック特性に応じて選ぶのが推奨です。
並行リクエスト時の整合性
複数スレッド・複数コルーチンから同時に状態更新が走ると、失敗カウンタの値がレースコンディションで狂います。内部状態は原子的操作(CAS、Mutex)で守る必要があります。
class ConcurrentSafeBreaker {
private lock = Promise.resolve();
async call<T>(action: () => Promise<T>): Promise<T> {
let release!: () => void;
const next = new Promise<void>((res) => (release = res));
const prev = this.lock;
this.lock = next;
await prev;
try {
return await this.protectedCall(action);
} finally {
release();
}
}
private async protectedCall<T>(action: () => Promise<T>): Promise<T> {
// 実装詳細は省略
return action();
}
}
分散環境での状態共有
水平スケールしている API サーバー群で各インスタンスが独立にブレーカーを持つ場合、ある瞬間にあるインスタンスだけが Open、別は Closed といった非対称が生まれます。これは「集合知としての健全性」を活かす意味で問題ない場合もありますが、Redis などで状態を共有する選択肢もあります。共有のオーバーヘッドと精度のトレードオフを評価しましょう。
Bulkhead との併用
サーキットブレーカー単独では「呼び出し中のリクエスト」がまだリソースを消費し続けます。Bulkhead パターンで接続プールやセマフォを分離すると、一つの依存先の問題が他に波及しなくなります。両者は補完関係です。
ヘルスチェックとの違い
サーキットブレーカーは「呼び出し結果」から判断するのに対し、ヘルスチェックは「能動的な疎通確認」です。ヘルスチェックは下流の状態を早期に知れる一方、本物のトラフィックでしか分からない問題もあります。両方を組み合わせるのが現実的です。
Adaptive Concurrency Limit
固定閾値ではなく、レイテンシをフィードバックにしてリアルタイムに同時実行数を調整する方式(Netflix の adaptive concurrency limits、Envoy adaptive concurrency filter など)が登場しています。サーキットブレーカーの拡張系として注目されています。
運用時の追加トピック
ブレーカーの粒度設計
ブレーカーをどの粒度で持つかは設計上の重要な決定です。代表的な分割方針を整理します。
- 依存先サービス単位: 「決済サービス全体」に 1 つ。実装が単純だが、片方の API だけ壊れているケースを区別できない。
- エンドポイント単位: 「決済サービスの POST /charge」「GET /status」を分ける。粒度は適切だが管理対象が増える。
- テナント単位: マルチテナント SaaS で特定テナントの集中アクセスから他テナントを守る目的。
- メソッド × データ単位: たとえば書き込み系と読み取り系で完全に分ける。
実運用では「サービス単位 + 重要エンドポイント単位」の二段構成がバランスの取れた選択になることが多いです。
ヘルス信号としての公開
ブレーカーの状態は内部実装の詳細と捉えがちですが、これを /healthz や Prometheus メトリクスとして外部公開することで、上位の依存サービスやロードバランサが意思決定に使えます。たとえば「依存先のうち半数以上のブレーカーが Open ならばこのインスタンス自体を unhealthy とみなす」といった連鎖判断が可能になります。
Polly / Resilience4j / opossum の比較
| ライブラリ | 言語/環境 | 特徴 |
|---|---|---|
| Polly | .NET | 流暢な DSL、Retry/Bulkhead と統合 |
| Resilience4j | Java/JVM | 関数型 API、メトリクス標準対応 |
| opossum | Node.js | EventEmitter ベース、簡単な統合 |
| gobreaker | Go | シンプル、依存ゼロ |
| pybreaker | Python | デコレータベース |
新規実装よりも既存ライブラリの採用を強く推奨します。レースコンディションやエッジケースに既に対処されています。
カオスエンジニアリングでの検証
ブレーカーが正しく動くかは、本番に近い環境で意図的に障害を起こしてみないと分かりません。Chaos Mesh、AWS Fault Injection Simulator、Gremlin などを使い、依存先にレイテンシ注入やエラー注入を行い、Open への遷移、フォールバックの起動、復旧時の Closed 復帰を一連で検証します。
# Chaos Mesh での HTTP レイテンシ注入例
apiVersion: chaos-mesh.org/v1alpha1
kind: HTTPChaos
metadata:
name: payment-latency
spec:
mode: all
selector:
namespaces:
- production
labelSelectors:
app: payment-service
target: Response
port: 8080
delay: 5s
duration: 5m
ケーススタディと設計ヒント
ケース 1: 決済 API の保護
EC サイトで決済代行サービスを呼び出すケースを考えます。決済代行が障害を起こすと、注文確定処理全体が止まります。サーキットブレーカーを導入し、Open 時には「後ほど決済完了通知をお送りします」という遅延応答に切り替える設計が有効です。注文自体は受け付け、決済処理を非同期キューに退避することで、ユーザー体験とビジネス機会損失を抑えられます。
ケース 2: 検索サービスのフォールバック
商品検索 API が遅延しているとき、サーキットブレーカーを開いて簡易的なキャッシュ済み結果や「人気商品」を返すフォールバックに切り替えます。検索精度は落ちますが、サイトが完全停止するよりは遥かにマシです。
ケース 3: 認証サービスの慎重設計
認証サービスはフォールバックが難しい代表例です。「認証情報が確認できないからログインさせる」では完全な脆弱性になります。この場合、ブレーカーは Open 時に明確にエラーを返し、しかし上流のリトライを抑制してリソース枯渇を防ぐ役割に徹します。フォールバックは諦め、Fail Fast 機能だけを使う設計判断もあり得ます。
サービスメッシュとの統合
Istio、Linkerd、Consul などのサービスメッシュは、サイドカープロキシ層でサーキットブレーカーを提供します。アプリケーションコードを変更せずに、ポリシーとして宣言できる点が魅力です。ただし、業務的に意味のあるフォールバック(「決済不可なら遅延処理」など)はアプリ側に実装する必要があり、両者の役割分担が重要です。
# Istio DestinationRule の例
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-circuit-breaker
spec:
host: payment-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 10
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 50
失敗の分類
「失敗」と一口に言っても、すべてを同列に扱うべきではありません。
- Transient(一時的): ネットワーク瞬断、瞬間的な高負荷。リトライで解決する可能性が高い。
- Persistent(永続的): 構成エラー、認証エラー。リトライしても無意味。
- Programmatic(業務的): 4xx エラー。クライアント側のバグ。
サーキットブレーカーは Transient と Persistent には反応すべきで、Programmatic には反応すべきではありません。HTTP ステータスコード別にカウント対象を制御するのが望ましいです。
チューニングのためのチェックリスト
- 失敗の定義は適切か(タイムアウトと接続失敗を含めているか)
- 失敗閾値は本番トラフィック量に対して適切か
- Open 状態の保持時間は下流の典型的な復旧時間と整合しているか
- Half-Open での試行数は控えめか
- フォールバックは業務的に有効な代替を返しているか
- メトリクスとアラートが整備されているか
- ブレーカーの粒度はサービス特性に合っているか
- 並行アクセス時の状態管理は安全か
- カオスエンジニアリングで動作確認したか
これらを定期的に見直し、本番運用しながら継続的にチューニングしていくことが重要です。
実装上の追加注意点
サーキットブレーカーは「動いているように見えて実は機能していない」状態に陥りやすいコンポーネントです。本番で実際にブレーカーが Open になった瞬間を観測するまでは、設定が正しいか確証が持てません。したがって、リリース直後は意図的に下流障害を起こしてみる「ゲームデー」を実施し、ブレーカー、フォールバック、アラート、復旧フローが連携して動作することを必ず確認しましょう。設計と検証をワンセットで考えることが、レジリエンスを単なる飾りに終わらせないための鍵です。
また、サーキットブレーカーは Retry、Timeout、Bulkhead、Rate Limiter など他のレジリエンスパターンと組み合わせて初めて真価を発揮します。単独で導入しても効果は限定的なので、レジリエンス戦略全体の中での位置づけを意識して採用しましょう。
参考リソース
- Martin Fowler - CircuitBreaker
- Microsoft Azure Architecture Center - Circuit Breaker pattern
- AWS Builders Library - Avoiding fallback in distributed systems
- Resilience4j Documentation - CircuitBreaker