決済プロバイダーが、50 ミリ秒ではなく 10 秒で応答するようになったとします。チェックアウトサービスは呼び出しを続け、リクエストは待つ間ずっとワーカーを占有します。1 分もしないうちに、決済プロバイダーを必要としないリクエストの分も含めて、チェックアウトには空いているワーカーがなくなります。そして、チェックアウトを呼び出しているサービスもタイムアウトし始めます。遅い依存先が 1 つあっただけで、複数のチームにまたがる障害になってしまったのです。
サーキットブレーカーは、明らかに失敗している依存先の呼び出しをしばらくの間拒否し、代わりにすぐ応答することで、この連鎖を止めます。この記事では、状態遷移図を使って 3 つの状態を説明し、ブレーカーが作動するタイミングを決めるしきい値、タイムアウトやリトライやバルクヘッドとの組み合わせ方、よいフォールバックの条件、そしてブレーカーを置く場所(コードの中かサービスメッシュか)を扱います。
障害が連鎖する理由
計算は単純で、容赦がありません。ワーカースレッドが 200 本あり、50 ms で応答する依存先を呼び出すサービスは、その経路で毎秒およそ 4,000 リクエストを処理できます。依存先が 10 秒のタイムアウトまで遅くなると、同じ 200 本のスレッドで処理できるのは毎秒 20 リクエストです。サービス自体には何の不具合もありません。単に、待つことで処理能力が使い果たされたのです。
リトライは事態を悪化させます。3 層のサービスがそれぞれ 3 回ずつリトライすると、1 件のユーザーリクエストが、最下層で苦しんでいる依存先への 27 回の呼び出しに膨れ上がることがあります。しかも、依存先が最も耐えられないタイミングで、です。
Michael Nygard は 2007 年に、本番環境向けのソフトウェア構築に関する著書でサーキットブレーカーを紹介しました。そして Martin Fowler の解説によって、サービス設計の標準的な要素になりました。依存先への呼び出しをラップし、失敗を数え、しきい値を超えたらしばらく呼び出しをやめる、という考え方です。
3 つの状態
| 状態 | 呼び出しはどうなるか | 遷移先 |
|---|---|---|
| クローズ(Closed) | 呼び出しは通り、結果はスライディングウィンドウに記録される | 失敗率または低速呼び出し率がしきい値を超えるとオープン |
| オープン(Open) | 呼び出しは依存先に触れずに即座に失敗し、フォールバックが応答する | 待機時間のあとハーフオープン |
| ハーフオープン(Half-open) | 少数の試行呼び出しだけを通し、残りは引き続き即座に失敗する | 試行が成功すればクローズ、失敗すればオープン |
オープン状態は、2 つの役割を同時に果たします。即座に失敗することでワーカーがすぐに解放されるため、自分のサービスを守ります。そして、増え続けるリトライの山ではなく、回復の間トラフィックから解放される時間を与えるため、依存先も守ります。
ハーフオープン状態があるのは、ブレーカーが試してみなければ依存先が回復したかどうかを知る方法がないからです。全トラフィックではなく少数の呼び出しだけを通すことで、戻ってきた直後の依存先を再び倒すことなく回復を確かめられます。
何を失敗として数えるか
これは意識的に決めてください。選択を誤ると、間違った理由でブレーカーが開いてしまいます。
- 数える: タイムアウト、接続エラー、5xx レスポンス、そして低速呼び出しのしきい値より遅い呼び出し。
- 数えない: 400 や 404 のような 4xx レスポンス。これは依存先の障害ではなく不正なリクエストを表しており、1 つの行儀の悪いクライアントのせいで全員が遮断されるべきではありません。
- ケースごとに判断する: 429 Too Many Requests。依存先が自分を守っているという意味で、多くの場合、手を引く十分な理由になります。
低速な呼び出しは、エラーと同じくらい重要です。すべてのリクエストに 8 秒かけて正常に応答する依存先は、対話型の API にとっては障害を起こしている依存先です。
しきい値の選び方
JVM で広く使われているブレーカーライブラリ Resilience4j には、控えめな既定値があります。多くのチームが一度も変更しないので、知っておく価値があります。
| 設定 | 既定値 | 意味 |
|---|---|---|
failureRateThreshold |
50% | 記録された呼び出しの半分が失敗したら開く |
slidingWindowType / slidingWindowSize |
回数ベース、100 | 直近 100 回の呼び出しで判断する |
minimumNumberOfCalls |
100 | 100 回記録されるまで判断しない |
slowCallDurationThreshold |
60,000 ms | これより遅い呼び出しを低速とみなす |
slowCallRateThreshold |
100% | すべての呼び出しが低速なときだけ開く |
waitDurationInOpenState |
60,000 ms | 1 分間オープンのままにする |
permittedNumberOfCallsInHalfOpenState |
10 | 10 回の試行呼び出しで回復を判断する |
対話型の API にとって、低速呼び出しの既定値は実質的に無効です。応答を 60 秒も待つ人はいません。妥当な出発点は、レイテンシー目標に近い低速呼び出しのしきい値、50%〜80% の低速呼び出し率、そして 10〜30 秒のオープン待機時間です。minimumNumberOfCalls は意味のある値にしておきましょう。トラフィックの少ないエンドポイントでは、3 回中 2 回の失敗は障害ではなくノイズです。1 日の中でトラフィックが大きく変動する場合は、回数ベースより時間ベースのウィンドウのほうがうまく機能します。
タイムアウト、リトライ、バルクヘッド
サーキットブレーカーは、組み合わせて初めてうまく機能する 4 つの道具の 1 つです。
- まずタイムアウトです。 タイムアウトがなければ、遅い呼び出しは決して失敗しないので、ブレーカーは数えるものを何も見られません。各タイムアウトは呼び出し元自身のタイムアウトより短く設定し、期限を呼び出しチェーンに沿って伝播させます。そうすれば、すでに見捨てられたリクエストの処理を誰かが続けることはありません。
- リトライには上限が必要です。 リトライするのは冪等な操作だけにし、AWS Architecture Blog で説明されているジッター付きの指数バックオフを使い、すべての層ではなく 1 つの層でだけリトライし、リトライをトラフィックの一定割合に制限します。Envoy のリトライバジェットは、有効にすると既定でアクティブなリクエストの 20% です。
- オープンのブレーカーに対してリトライしてはいけません。 Resilience4j は既定でリトライをブレーカーの外側に適用するため、各試行はブレーカーを通ります。すでに判断を下したブレーカーを叩き続けるのではなく、「call not permitted」エラーでリトライが止まるように設定してください。
- バルクヘッドで隔離します。 依存先ごとに専用のコネクションプールや同時実行数の上限を持たせれば、遅い決済プロバイダーが消費できるのは自分のプールだけになり、サービス全体のワーカーを使い果たすことはありません。
意味のあるフォールバック
ブレーカーがオープンのときは、何かが応答しなければなりません。適切なフォールバックは、そのデータが何に使われるかによって決まります。
| 機能 | よいフォールバック | 悪いフォールバック |
|---|---|---|
| おすすめ | 人気商品を出す、またはセクションを隠す | おすすめサービスをループでリトライする |
| ユーザーのアバター | プレースホルダー画像 | 画像サービスが応答するまでページをブロックする |
| 配送日の見込み | 注記付きのキャッシュした見込み | でっち上げた配送日 |
| 価格や支払い | 明確なエラーと再試行の手段 | 推測した価格や、黙って飛ばした請求 |
フォールバックは安価でなければならず、たった今失敗したものに依存してはいけません。正直な答えが「今は動いていません」なら、明確なエラーですぐに失敗させましょう。それでもタイムアウトよりはるかにましです。
ブレーカーを置く場所
| プロセス内のライブラリ | サービスメッシュやプロキシ | |
|---|---|---|
| 例 | Resilience4j(Java)、Polly(.NET)、gobreaker(Go)、opossum(Node.js) | Envoy のサーキットブレーキングと外れ値検出 |
| 見えるもの | 例外、業務レベルの失敗、フォールバック | 接続、リクエスト、レスポンスコード |
| フォールバック | 可能。コードの中に書く | 不可。呼び出し元はエラーを受け取る |
| チーム間の一貫性 | 各チームが個別に設定する | 1 つのポリシーをどこにでも適用する |
Envoy のモデルは教科書的なモデルとは異なるので、理解しておく価値があります。Envoy のサーキットブレーカーは上流クラスターごとの同時実行数の上限で、既定では接続 1,024、保留中のリクエスト 1,024、アクティブなリクエスト 1,024、同時リトライ 3 です。ホスト単位の障害検出は外れ値検出という別の機能で、既定では 5xx レスポンスが 5 回連続したホストを 30 秒間除外し、繰り返し除外される場合はより長くします。また、クラスターの 10% を超えて除外することはありません。これらのプロキシがどこに置かれるかは、サービスメッシュのアーキテクチャ図で示しています。
多くのチームは両方を使います。一貫した上限と不調なホストの除外にはメッシュを、アプリケーションが本当のフォールバックを必要とする場所にはライブラリを使うのです。このパターンを広めた Netflix の Hystrix は 2018 年からメンテナンスモードで、JVM では Resilience4j が一般的な後継です。
監視とテスト
黙って開くブレーカーは、障害を謎に変えてしまいます。状態の変化はすべてログイベントとメトリクスとして出力し、ブレーカーが開いたままならアラートを出し、ブレーカーの状態を依存先のレイテンシーと同じダッシュボードに表示しましょう。そのうえでテストします。ステージング環境で依存先に遅延やエラーを注入し、ブレーカーが開き、フォールバックが応答し、ブレーカーが再び閉じるのを確認してください。メトリクスとトレーシングの側面は、オブザーバビリティのアーキテクチャ図で扱っています。
サーキットブレーカーの図
ダイアグラムの内容
- CLOSED
- OPEN
- HALF-OPEN
- Checkout service (caller)
- Payment provider
- Fallback: clear error, retry later
- failure rate ≥ 50% of the last 100 calls
- after a 60 s wait
- a trial call fails
- trial calls succeed
- charge(), 2 s timeout, when closed or half-open
- when open: no call, fail fast
図の読み方は次のとおりです。
- 3 つの状態。 クローズは通常の状態、オープンは即座に失敗する状態、ハーフオープンは回復を確かめる状態です。点が初期状態を示します。
- 遷移には条件が書かれています。 直近 100 回の呼び出しで失敗率 50% になるとブレーカーが開き、60 秒待つとハーフオープンに移り、試行呼び出しの結果によって閉じるか再び開くかが決まります。
- 下の呼び出し経路は、呼び出し元、ブレーカーの背後にある決済プロバイダー、そしてブレーカーがオープンの間に応答するフォールバックを示します。
- Markdown ブロックには、各設定とその既定値をまとめています。
レジリエンスのパターンをわかりやすく描くには
- ブレーカーは状態機械として描きます。 文章での説明では、オープンとハーフオープンの違いがぼやけてしまいます。図にすれば一目瞭然です。
- 遷移には数字を書きます。 「失敗すると開く」では議論を招きますが、「直近 100 回の 50% で開く」ならレビューにつながります。
- フォールバックの経路を示します。 ユーザーに何が見えるかも含めます。
- タイムアウトとリトライは呼び出し経路の図に書き込みます。 そうすれば、レビュアーが層ごとのリトライ回数を掛け合わせて確認できます。
状態機械の記法について詳しくは、状態遷移図の例を参照してください。サービス間のどこにブレーカーが置かれるかは、マイクロサービスのアーキテクチャ図で示しています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- UML パックの 状態 ノードを 3 つ追加します。Closed、Open、Half-open です。それぞれ緑、赤、琥珀色にします。
- 初期状態として基本パックの小さな 楕円 の図形を追加し、Closed につなぎます。
- 遷移をラベル付きのエッジとして描きます。失敗のしきい値、待機時間、試行の成功、試行の失敗です。各状態の横には、そこで呼び出しがどうなるかを書いた基本パックの テキスト を追加します。
- 状態機械の下に、呼び出し元用の基本パックの ボックス、決済プロバイダー用の C4 モデルパックの 外部システム、フォールバック用にもう 1 つ ボックス を追加し、オープン状態の経路は破線のエッジで描きます。
- 設定と既定値を書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 8 個で、Free プランの範囲に収まります。
よくある質問
サーキットブレーカーの 3 つの状態とは何ですか? 呼び出しを通して失敗を数えるクローズ、待機時間の間は呼び出しを即座に失敗させるオープン、そして少数の試行呼び出しによって再びクローズにするかオープンに戻すかを決めるハーフオープンです。
サーキットブレーカーとリトライの違いは何ですか? リトライは、成功を期待して失敗した呼び出しを繰り返します。サーキットブレーカーは、失敗が頻繁なときに呼び出しを完全に止めます。両方を使う場合、リトライには上限を設け、ブレーカーがオープンのときは止めるべきです。
4xx エラーでサーキットブレーカーを開くべきですか? 通常は開くべきではありません。400 や 404 は、依存先の障害ではなく、リクエストが間違っていたことを意味します。代わりに、タイムアウト、接続エラー、5xx レスポンス、低速な呼び出しを数えてください。
サービスメッシュを使っていても、サーキットブレーカーは必要ですか? メッシュはコードを変更せずに、同時実行数の上限と不調なホストの除外を提供します。それでも、キャッシュした値や機能を絞った動作のような本当のフォールバックが必要な場所には、プロセス内のブレーカーが必要です。
オープン状態の待機時間はどれくらいがよいですか? 苦しんでいる依存先が回復できるだけ長く、回復後にユーザーが長い障害を目にしないだけ短くします。対話型の API では、10〜30 秒がよくある出発点です。