連鎖的な障害:1つのドミノがシステム全体を倒すとき
マイクロサービスアーキテクチャでは、10個のサービスが互いに呼び出し合うような構成は珍しくありません。しかし、次のようなシナリオを考えてみてください。サードパーティの決済APIが突然10秒の遅延、あるいは完全にダウンしたとします。すると、あなたの注文サービスは懸命にリクエストを送り続け、応答を待ち続けます。
その結果、ワーカースレッドが使い果たされ、ボトルネックが発生します。この時、決済機能だけでなく、カートの閲覧や検索機能までもが道連れになって停止してしまいます。これが、バックエンドエンジニアにとって最悪のシナリオであるカスケード障害(連鎖的な障害)です。
これを防ぐために, サーキットブレーカー(遮断器モデル)は必須のソリューションです。これは家庭にある電気のブレーカーのように機能します。電流(リクエスト)が過負荷になったり異常が発生したりすると、回路を自動的に遮断し、背後にあるすべての機器(システム)を故障から守ります。
なぜタイムアウトとリトライだけでは不十分なのか?
依存関係のエラーを処理するために、多くの人が2つの基本的な方法を使いますが、それらには致命的な弱点があります。
1. リトライ(再試行)メカニズム
相手のサービスが過負荷になっている場合、3〜5回のリトライリクエストを送り続けることは、相手をさらに早くダウンさせるだけです。これは、疲れ果てて倒れそうな人に「もっと走れ」と強制するようなものです。
2. タイムアウト(待機時間)
タイムアウトを設定することで、リソースを早めに解放できます。しかし、1,000個のリクエストが同時に5秒のタイムアウトを待っている場合、システムはそれらの接続を維持するために依然として大量のRAMとCPUを消費します。
3. サーキットブレーカー(インテリジェントな遮断器)
これは「フェイルファスト(早期失敗)」メカニズムです。閉まっているドアに頭をぶつけ続けるのではなく、システムがエラー率を監視します。エラー率が閾値(例:10秒間に50%のエラー)を超えると、即座に回路を遮断(Open)します。それ以降のリクエストは、故障中のサービスを呼び出して時間を無駄にすることなく、即座にエラー通知やバックアップデータ(フォールバック)を受け取ります。
実践的な評価:メリットとデメリット
明確なメリット:
- 影響範囲の隔離: サービスAのエラーがサービスBに波及するのを防ぎます。
- システムの自己修復: ハーフオープン(Half-Open)メカニズムにより、サービスが安定した際にシステムが自動的に試行し、回路を閉じます。
- よりスムーズなUX: ユーザーは、終わりのないローディングアイコンを見る代わりに、「現在メンテナンス中です」というレスポンスを即座に受け取れます。
課題:
- パラメータ設定: 回路を遮断するためのエラー閾値を50%にするか20%にするかは、実際のモニタリングデータに基づいて決定する必要があります。
- データの整合性: フォールバックデータが、その後のビジネスロジックに悪影響を与えないように注意する必要があります。
Node.jsプロジェクトへのOpossumの導入
Node.jsのエコシステムにおいて、Opossumは現在最も標準的なライブラリです。軽量で、Closed、Open、Half-Openのすべての状態をサポートしており、Async/Await関数とも非常によく統合されています。
Opossumの複雑なオプションを構成する際、私はよく toolcraft.app/ja/tools/developer/json-formatter を使ってJSON構造をチェックします。これにより、サーキットブレーカーが意図通りに動作しなくなるような単純な構文エラーを防ぐことができます。
ステップ1:クイックインストール
npm install opossum
ステップ2:実践的なサンプルコード
商品情報を取得するサービスを呼び出す必要があると仮定しましょう。そのロジックをサーキットブレーカーでラップします。
const CircuitBreaker = require('opossum');
async function callExternalAPI() {
// 実際のAPI呼び出しをシミュレート
if (Math.random() > 0.7) throw new Error('APIがダウンしました!');
return { status: 'success', data: '商品A' };
}
const options = {
timeout: 3000, // 3秒間APIの応答がない場合に遮断
errorThresholdPercentage: 50, // リクエストの50%以上が失敗した場合に回路をオープンにする
resetTimeout: 15000 // 15秒後に再接続を試行
};
const breaker = new CircuitBreaker(callExternalAPI, options);
// フォールバック(代替データ)の設定
breaker.fallback(() => ({ status: 'fallback', data: 'キャッシュからのデータ(オフライン)' }));
// 実行
breaker.fire()
.then(console.log)
.catch(console.error);
ステップ3:イベントによる監視
サーキットブレーカーを「ブラックボックス」にしてはいけません。イベントをリッスンしてログをGrafanaに送ったり、Slackにアラートを飛ばしたりしましょう。
breaker.on('open', () => console.error('--- 回路オープン:サービスが深刻なエラーです。呼び出しを停止します! ---'));
breaker.on('close', () => console.info('--- 回路クローズ:サービスが復旧しました。通常稼働します ---'));
breaker.on('halfOpen', () => console.log('--- ハーフオープン:テストリクエストを送信中... ---'));
運用における「血の滲むような」教訓
自業自得な事態を避けるための3つの注意点を紹介します。
- 過敏になりすぎない:
errorThresholdPercentageを低すぎ(20%以下)に設定すると、一時的なネットワークの揺らぎだけで頻繁に回路が跳ねてしまう可能性があります。 - インテリジェントなフォールバック戦略: 単にエラーを返すだけではなく、キャッシュ内の古いデータやデフォルト値を返すように努め、ユーザーフローが中断されないようにしましょう。
- ヘルスチェックのバイパス: システムのヘルスチェック(Health Check)用ルートがサーキットブレーカーによってブロックされないようにしてください。さもないと、Kubernetesが誤ってPodを強制終了(kill)してしまう可能性があります。
バイパスが必要なURLをフィルタリングするために複雑な正規表現(Regex)を扱う場合は、toolcraft.appのようなオンラインツールを使って事前にテストすることをお勧めします。これにより、コードを修正してデプロイを繰り返すデバッグの時間を大幅に節約できます。
おわりに
サーキットブレーカーは単なる技術ではなく、システムを能動的に守るための思考法です. Opossumを使用することで、Node.jsアプリケーションは外部の障害に対してより堅牢になります。サードパーティのサービスがどのような状態であっても、システムが常に安定して稼働し続けられるよう、重要なサービスに今すぐ適用しましょう。

