背景
EnvoyにはCircuit Breakerがあり、依存するサービスがスローダウンした際にそれ以上リクエスト送らず即座に503を返すことでサービスが死なないようにする仕組みがあります。
しかしパラメータの調節が難しく、期待した挙動にならないことが多いため1つ1つのパラメータの挙動をメトリクスとともに書いておきます。
環境
- Envoy v1.22.0
構成図は以下です。
検証
Circuit Breakerのパラメータ
EnvoyのCircuit Breakerのパラメータは以下です。
パラメータ | デフォルト値 | 説明 |
---|---|---|
priority | DEFAULT | DEFAULT と HIGH のプライオリティを設定する |
max_connections | 1024 | upstreamクラスタに接続する最大数 |
max_pending_requests | 1024 | upstreamクラスタに許可する保留中のリクエストの最大数 |
max_requests | 1024 | upstreamクラスタに行う並列リクエストの最大数。 その時点の並列数なのでRPSと同義ではない点に注意 |
max_retries | 3 | upstreamクラスタに許可する並列再試行の最大回数 |
retry_budget | - | アクティブなリクエストの数に対する同時再試行の制限 |
track_remaining | false | サーキットブレーカーが開くまでの残りのリソース数を公開する |
max_connection_pools | unlimited | クラスタごとのコネクションプールの最大数 |
stats
Circuit Breaker発動時にチェックするstats情報は以下です。
stats | 説明 |
---|---|
cx_open | max_connections によるCircuit BreakerがOpenした際に0→1になる |
cx_pool_open | max_connection_pools によるCircuit BreakerがOpenした際に0→1になる |
rq_pending_open | max_pending_requests によるCircuit BreakerがOpenした際に0→1になる |
rq_open | max_requests によるCircuit BreakerがOpenした際に0→1になる |
rq_retry_open | max_retries によるCircuit BreakerがOpenした際に0→1になる |
検証パターン
Circuit Breakerでよく使われるパラメータは
- max_connections
- max_pending_requests
- max_requests
で、これらを以下のパターンで検証します。
- Circuit Breakerの設定無し
- max_connectionsのみ
- max_pending_requestsのみ
- max_requestsのみ
- max_connectionsとmax_pending_requests
- max_requestsとmax_pending_requests
- max_connectionsとmax_requests
k6
基本的にk6で以下のスクリプトを実行します。VUSは適宜調整します。
import http from "k6/http"; import { check } from "k6"; export const options = { vus: 30, duration: "1m", }; export default function () { const res = http.get("http://localhost:8080/test"); check(res, { "is status 200": (r) => r.status === 200, }); }
Circuit Breakerの設定無し
普通にbackendまで貫通することを確認します。
メトリクス
max_connectionsのみ
ではまずmax_connections
を制限してみます。
circuit_breakers: thresholds: max_connections: 10
VUS: 30
仮説としては30 - 10 = 20リクエストあふれるのでサーキットブレーカーはOpenしそうです。
一部エラーが返りました。
メトリクス
結果
仮説通りサーキットブレーカーはOpenするものの、その後の挙動が503ではありませんでした。
active connectionは制限されているものの、残りのリクエストがpending requestとしてconnection poolに流されたためです。
- active connectionはmax_connectionsで制限されている
- そのためconnection poolの数が増えている
- サーキットブレーカーはOpenしている
- 503にはならない
- timeoutして504が一部出ている
VUS: 100
やや負荷を上げてみます。先程の例を考慮すると503は出なそうに見えます。
エラー割合が増えました。
メトリクス
結果
仮説通り503ではなく504が増えました。
- active connectionはmax_connectionsで制限されている
- そのためconnection poolの数が増えている
- サーキットブレーカーはOpenしている
- 503にはならない
- timeoutして504が多数出ている
max_pending_requestsのみ
次はmax_pending_requests
を検証します。
circuit_breakers: thresholds: max_pending_requests: 15
VUS: 30
仮説としてはactive connectionが増え続け、pending requestは増えなそうです。
エラーは発生しませんでした。
メトリクス
結果
仮説通りでした。
- 新規コネクションとしてハンドリングされるのでpending requestsが発生しなかった
- サーキットブレーカーはOpenしなかった
- 503は返らなかった
VUS: 100
仮説としては先程と同じくactive connectionでハンドリングされると思われます。
一部503が発生するようになりました。
メトリクス
結果
仮説と異なり一部503が出ていました。しかしpending requestは増えておらず、サーキットブレーカーもOpenしていませんでした。
- 一部503が発生
- pending request(rq_pending_active)は上限の15に達していない
- サービスブレーカーのOpenは観測されなかった
- pending_overflowが発生した
- pending_overflowはドキュメント上はサーキットブレーカーによるもの
Total requests that overflowed connection pool or requests (mainly for HTTP/2 and above) circuit breaking and were failed
ref: https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats
VUS: 200
もう少し負荷を上げてみます。先程503が出たのでもしかしたらpending requestsが増えるかもしれません。
エラーが急増しました。
メトリクス
結果
仮説通りpending requestが増え503が返るようになりました。
- 短い期間だがサーキットブレーカーのOpenを観測できた
- pending request(rq_pending_active)は上限の15に達した
- Openするとpending_overflowが大量にでるようになった
- 大半は503で即時返るようになった
VUS: 200, max_pending_requests: 200
今度はmax_pending_requests: 200
を増やしてみます。
仮説としては全て200で捌けるようになる、です。
メトリクス
結果
仮説と異なり503が多く出ました。しかしサーキットブレーカーはOpenしませんでした。
- 503は大量に出た
- サーキットブレーカーはOpenしていない
- pending_oveflowもない
- upstream_rq_pending_failure_eject が同数出ていた
- connection pool の connection error
Total requests that were failed due to a connection pool connection failure or remote connection termination
max_requestsのみ
次はHTTP/2の設定でよく見るmax_requestsです。HTTP/1.xでも利用可能です。
circuit_breakers: thresholds: max_requests: 10
注意としてその時点の並列数なのでRPSで計算する際はレイテンシを考慮する必要があります。
例えば平均レイテンシW
が(or 50th percentile)200msでスループットλ
が100RPSならば、リトルの法則から同時処理リクエスト数L
は
と言えます。
ただ実際のところこれは平均値であり遊びがなく、そのまま使うとかなりの割合でCircuit Breakerが発動するので最適とは言えません。
Circuit Breakerを必要とするような状況では基本的にキャパシティを超えてレイテンシが悪化し始める状態なので、現実的にはRPSをそのまま使っても問題ないです。
VUS: 30
仮説としては並行で10リクエストに制限されるのでサーキットブレーカーがOpenし503が返ります。
メトリクス
結果
仮説通りの挙動でした。
- connectionの制限はないため、active connection数はVUS分生成された
- active requestは制限通り10を保った
- コネクションは生成されたが、pending requestにはならず即時503になっている
- きっちり10requestsを保持し、バースト耐性がないことが分かる
- サーキットブレーカーがOpenした
- pending_overflowが多数出た
- 503が返った
VUS: 100
VUS: 30
でサーキットブレーカーがOpenしたので省略します。
max_connectionsとmax_pending_requestsを併用
次からは併用パターンです。
circuit_breakers: thresholds: max_connections: 10 max_pending_requests: 15
VUS: 30
仮説としては30(VUS) - 10 - 15 = 5リクエスト
が必ずあふれるので、サーキットブレーカーはOpenすると考えられます。
メトリクス
結果
仮設通りの結果でした。
- max_connections、max_pending_requestsの両方サーキットブレーカーがOpenしている
- 503で即時返っている
- 一部504も出ている
VUS: 20
仮説としてはmax_connectionsは制限されますがmax_pending_requestsを超えないので捌ける、です。
メトリクス
結果
仮説通り503にはなりませんでした。しかし一部timeoutが出ていました。
- 503は返らない
- timeoutによる504が発生した
max_requestsとmax_pending_requestsを併用
circuit_breakers: thresholds: max_pending_requests: 30 max_requests: 10
VUS: 50
仮説としてはmax_requests
で制限されるので、pending_requestsは効果がなく、503が返ると考えられます。
メトリクス
結果
仮説通りの挙動でした。
- max_requestsのサーキットブレーカーがOpen
- pending requestsは増えない
- 即座に503が返る
- コネクション数はVUSに相関する
- 504は出ない
max_requestsとmax_connectionsを併用
最後の組み合わせです。
circuit_breakers: thresholds: max_connections: 10 max_requests: 15
VUS: 30
max_requestsを超えて503が返ると思われます。
メトリクス
結果
仮説とは違いました。
- 503は返らない
- max_connections < max_requestsの場合、max_requestsのサーキットブレーカーがOpenしない
- max_requestsはactive requestしかカウントしていない
max_connections>max_requestsにしてみる
先程の挙動から、max_connections
よりmax_requests
が大きいとmax_requests
は効果がなくなりました。
なので逆にしてみます。
circuit_breakers: thresholds: max_connections: 15 max_requests: 10
仮説としてはmax_requests
により503が返ります。
メトリクス
結果
仮説通りの結果になりました。
- max_requestsのサーキットブレーカーがOpenする
- 503が返る
結果一覧
結果を表でまとめると以下になります。
条件 | サーキットブレーカー | 503が返るか | 備考 |
---|---|---|---|
max_connections | Openする | 返らない | pending requestとして受け付けるのでHTTP/1.xなら後ろに流れない。しかしtimeoutによる504になる。 |
max_pending_requests | Openしたりしなかったり | 返る | リクエストが新規active connectionに流れるため中々pending requestとならず、サーキットのOpenが予測しづらい |
max_requests | Openする | 返る | pending requestが発生しない。max_requestsを超えたらすぐ503で返る バースト耐性がない |
max_connectionsとmax_pending_requests | 両方Openする | 返る | max_connectionsとmax_pending_requestsの合計が限界値となる |
max_requestsとmax_pending_requests | max_requestesがOpenする | 返る | max_requestsが限界値となり、max_pending_requestsがあっても意味はない |
max_connectionsとmax_requests | 小さい方がOpenする | (max_connection<max_requestsなら)返らない (max_connection>max_requestsなら)返る |
max_requestsが上限となる。 ただしmax_requestsにはactive requestしかカウントされないため、max_connections<max_requestsなら503にはならない。 |
考察
上記結果からどう設定すべきかを考察します。
- 1つだけだと柔軟性にかける
- max_connectionsを負荷試験でのRPS上限値とし、max_pending_requestsでバースト耐性を調整するのが良さそう
- HTTP/2ではmax_requestsを使うが、負荷試験のRPS上限値ではなく若干のバースト耐性分を上乗せしても良さそう
まとめ
EnvoyのCircuit Breakerの動作検証を行ってみました。
予想と異なる挙動も多く、パラメータの調整も非常に難しいことが分かりました。
サンプルコード
今回のサンプルコードはこちら。GrafanaのDashboard jsonも入れています。