サーキットブレーカーの状態機械
一言でいうと
サーキットブレーカーは、障害を直しません。障害が確実なときに素早く失敗して、呼び出し側のリソースが枯渇しないように守ります。
なぜ必要なのか
リトライだけでは足りません。ダウンストリームが本当に落ちているとき、リトライは状況を悪化させるだけです。そして呼び出し側にとっても、どうせ失敗する呼び出しを3秒ずつ待つのは、コネクションとスレッドの無駄です。
サーキットブレーカーは、「最近の結果を見ると、このサービスは今使えない」と判断すると、呼び出しをそもそも試行せず、すぐに失敗を返します。マイクロ秒単位で失敗するので、呼び出し側のリソースは枯渇せず、ダウンストリームは回復する息継ぎの時間を得ます。
どう動くのか
状態は3つです。
CLOSEDは正常です。すべての呼び出しが通り、結果をスライディングウィンドウに記録します。ウィンドウ内の失敗率がしきい値を超えると、OPENに移行します。
OPENは遮断です。すべての呼び出しが即座に拒否されます。waitDurationInOpenStateが経過すると、HALF_OPENに移行します。
HALF_OPENは試験です。決められた数の試験的な呼び出しだけを許可し、その結果でCLOSEDに復帰するか、OPENに戻ります。
設定値には正解はありませんが、出発点はあります。筆者が本番で使っている値は、次のとおりです。slidingWindowSize: 10、minimumNumberOfCalls: 5、failureRateThreshold: 50、slowCallDurationThreshold: 3s、waitDurationInOpenState: 30s、permittedNumberOfCallsInHalfOpenState: 3。サービスの性格に応じて、しきい値を調整します。決済のように失敗が致命的な場所は30–40%、通知のように寛容でもよい場所は60–70%です。
ここで最も重要な設計上の決定が1つあります。何を失敗として数えるかです。4xxは失敗ではありません。クライアントが間違って送ったものなので、サーキットが介入する事柄ではありません。実際の事故の事例があります。在庫サービスが特定の商品に400を返し始め、その400が失敗率に集計されたためにサーキットがOPENになり、正常な商品の取得まですべて遮断されました。サーキットは、5xx、タイムアウト、接続の失敗にだけ反応しなければなりません。
現場での姿
2番目によくある事故は、HALF_OPENのフラッピングです。permittedNumberOfCallsInHalfOpenStateを1にすると、試験的な呼び出し1件がたまたま失敗するたびに、再びOPENに落ちます。ダウンストリームが断続的にしか応答しない状況で、永遠にCLOSEDに戻れません。最小で3、普通は5–10を使います。
3番目。minimumNumberOfCallsのデフォルト値が100の実装が多くあります。呼び出し頻度が低いサービスでは、ウィンドウが埋まる前に障害が終わってしまい、サーキットが一度も開きません。「サーキットを入れたのに開きません」の半分は、この理由です。
そして、サーキットは必ずフォールバックと併用します。フォールバックがreturn nullなら、サーキットは障害をNullPointerExceptionに変えただけです。キャッシュされた値、縮小したレスポンス、または明確な案内のどれかでなければなりません。
サーキットブレーカーだけでは足りない
サーキットは、すでに悪くなった後で反応する仕組みです。その前後に一緒に置くべきものがあり、4つが揃って初めて1つの防御線になります。
タイムアウト: 最も基本的でありながら、最もよく抜け落ちます。タイムアウトがないと、サーキットが失敗を数える機会すらなく、呼び出し側のスレッドが無限に塞がれます。値は、相手の遅延の分布を見て決めますが、呼び出しの連鎖の内側に行くほど短くなければなりません。外側が3秒なのに内側が5秒だと、内側が答える前に外側がすでにあきらめていて、内側の作業がまるごと無駄になります。
バルクヘッド(bulkhead): 1つの相手に使える同時呼び出し数を、別に制限します。これがないと、遅くなったサービス1つが、共有のスレッドプールやコネクションプールをすべて占有して、そのサービスと無関係な機能まで一緒に止まります。障害が広がる経路の大部分は、この共有リソースです。
リトライ: サーキットとは逆の方向に作用するので、併用するときには規則が必要です。冪等な呼び出しにだけ、上限を設けて、間隔に乱数を混ぜます。そして、連鎖の複数の層でそれぞれリトライすると、掛け算になります。3つの層がそれぞれ3回ずつリトライすると、最悪の場合は27回です。リトライは、連鎖の中で1つの層だけが担うのが原則です。
フォールバック: サーキットが開いたときに何を返すか、です。前に述べたように、例外を再び投げるフォールバックは、何の意味もありません。良いフォールバックは、たいてい次の3つのどれかです。少し古いキャッシュの値、その部分だけを除いた縮小したレスポンス、または「今はこの情報を取得できませんでした」ということを、画面が理解できる形で知らせること。
最後に、この4つはテストされなければなりません。普段一度も開いたことのないサーキットは、本当の障害のときに初めて動作し、そのときフォールバックに欠陥があると、防御線がかえって障害を生みます。相手をわざと落としてみる訓練を定期的に行う理由が、これです。
次のラボですること
サーキットブレーカーを自分で実装します。状態遷移を目で見て、OPENではダウンストリームに本当に呼び出しが行かないことをカウンターで証明し、4xxを失敗として数えないようにし、最後に全体の遷移履歴をファイルに残します。