Carpe Diem

備忘録

Envoy Circuit Breakerの挙動を確認する

背景

EnvoyにはCircuit Breakerがあり、依存するサービスがスローダウンした際にそれ以上リクエスト送らず即座に503を返すことでサービスが死なないようにする仕組みがあります。

しかしパラメータの調節が難しく、期待した挙動にならないことが多いため1つ1つのパラメータの挙動をメトリクスとともに書いておきます。

環境

  • Envoy v1.22.0

構成図は以下です。

検証

Circuit Breakerのパラメータ

EnvoyのCircuit Breakerのパラメータは以下です。

パラメータ デフォルト値 説明
priority DEFAULT DEFAULTHIGH のプライオリティを設定する
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 クラスタごとのコネクションプールの最大数

ref: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#config-cluster-v3-circuitbreakers-thresholds

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になる

ref: https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats#circuit-breakers-statistics

検証パターン

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

ref: https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats.html

max_requestsのみ

次はHTTP/2の設定でよく見るmax_requestsです。HTTP/1.xでも利用可能です。

circuit_breakers:
    thresholds:
        max_requests: 10

注意としてその時点の並列数なのでRPSで計算する際はレイテンシを考慮する必要があります。

例えば平均レイテンシWが(or 50th percentile)200msでスループットλが100RPSならば、リトルの法則から同時処理リクエスト数L

L=λW
L=100 * 0.2
L=20

と言えます。

ただ実際のところこれは平均値であり遊びがなく、そのまま使うとかなりの割合で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も入れています。

github.com

参考