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クラスタに行う並列リクエストの最大数
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

検証パターン

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

VUS: 30

仮説としては並行で10リクエストに制限されるのでサーキットブレーカーがOpenし503が返ります。

メトリクス

結果

仮説通りの挙動でした。

  • connectionの制限はないため、active connection数はVUS分生成された
  • active requestは制限通り10を保った
    • コネクションは生成されたが、pending requestにはならず即時503になっている
  • サーキットブレーカーがOpenした
  • pending_overflowが多数出た
  • 503が返った
  • きっちり10req/secを保持し、バースト耐性がないことが分かる

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

参考