TT Lab
はじめる
学ぶ 学習パス コース

Envoyの内部構造

Envoy 二台、カウンター一つ

TT Labで続きを見る

目標

本物のレート制限サービス(envoyproxy/ratelimit)とRedisを起動し、同じサービスを呼ぶEnvoy2台が1つの上限を分け合う様子を、ローカル制限と並べて数えます。

なぜ重要なのか

ローカル制限だけを使うと、プロキシを増やすたびに上限が増えます。オートスケーリングの前では、防ごうとした急増が来るほど、門が広がります。グローバル制限はその問題を解決しますが、リクエストごとにサービスを1つ余分に経由させ、そのサービスが死んだときの動作(デフォルト: 通過)を知らないと、障害のときに誰もブロックされなかった理由が見つかりません。両方を自分で数えてみれば、2つをなぜ一緒に使うのかが、頭に残ります。

ステップ

  1. Redisを127.0.0.1:6379で、ディスクに書き込まないように(--save ''、--appendonly no)デーモンとして起動してください。そして/root/envd-rl/01-redis.txtに、redis-cli pingの出力とredis-cli config get saveの出力を、順に保存してください。
  2. /root/envd-rl/runtime/config/edge.yamlに、ドメインedgeのルールを2つ書いてください。ディスクリプターplan=freeは1時間に3回、plan=proは1時間に100回です。そしてイメージに入っているratelimit(envoyproxy/ratelimit)を起動してください。設定は環境変数で渡します: Redis127.0.0.1:6379、RUNTIME_ROOT=/root/envd-rl/runtime、RUNTIME_SUBDIRECTORY=.、RUNTIME_WATCH_ROOT=false、USE_STATSD=false、そしてHTTP8180・gRPC8181・デバッグ6070のポート(3つとも127.0.0.1)。
  3. アップストリームpython3 /opt/lab/envoy/echo.py 8092を起動し、/root/envd-rl/envoy-a.yamlで管理ポート9983、リスナー127.0.0.1:10083のEnvoyを起動してください(--base-id 21)。HTTPフィルターは、local_ratelimit(stat_prefix local_rl) → ratelimit(ドメインedge、クラスターratelimitへgRPC、failure_mode_deny: false、enable_x_ratelimit_headers: DRAFT_VERSION_03) → routerの順です。ルートは2つあります。/localは、そのルートにだけローカルのトークンバケット(トークン2個、60秒ごとに2個)を設定し、/は、そのルートにrate_limitsで、リクエストヘッダーx-planをディスクリプターのキーplanにします。
  4. /root/envd-rl/envoy-b.yamlに、1台目のEnvoyと同じで、管理ポート9984、リスナー127.0.0.1:10084の設定を書き、--base-id 22で起動してください。次に、2台のEnvoyにx-plan: proのリクエストを1つずつ送って、応答のx-ratelimit-remainingが続けて減るかを見て、2つの値を/root/envd-rl/04-remaining.txtにa=・b=の2行で書いてください(aを先に送ります)。
  5. x-plan: freeのリクエスト8個を、A(10083)・B(10084)に交互に(Aから)/へ送り、順番に/root/envd-rl/05-global.txtに、a1=코드、b1=코드、a2=코드 … b4=코드の8行で書いてください(プレースホルダーはステータスコードです)。
  6. 同じ方法で、今度は8個を/localへ送り、/root/envd-rl/06-local.txtにa1=코드 … b4=코드の8行で書いてください(プレースホルダーはステータスコードです)。
  7. ratelimitプロセスを止めたまま、Aにx-plan: freeのリクエストを1つ送り、そのステータスコードを/root/envd-rl/07-fail.txtにdown_code=で書いてください。その後、Aの統計cluster.echo.ratelimit.errorとcluster.echo.ratelimit.failure_mode_allowedの値を、error=・allowed=の2行で追記し、サービスを起動し直します。
  8. /root/envd-rl/08-report.mdに、次の4行を書き、その下に学んだことを4行以上書いてください。global_ok_total=(ステップ5の200の個数)、local_ok_total=(ステップ6の200の個数)、fail_open_code=(ステップ7のdown_code)、counted_in=(グローバル制限で実際に数を数えた場所: envoyまたはredis)です。

参考

数える場所から立てる — Redis

Redisを127.0.0.1:6379で、ディスクに書き込まないように(--save ''、--appendonly no)デーモンとして起動してください。そして/root/envd-rl/01-redis.txtに、redis-cli pingの出力とredis-cli config get saveの出力を、順に保存してください。

グローバルレート制限サービスは、状態を持ちません。何回来たかは、すべて後ろのストレージ(Redis)に置き、サービスは、ウィンドウ(window)ごとにキー1つを上げるだけです。そのため、サービスを複数台起動しても、上限が1つのまま維持されます。カウンターはウィンドウが過ぎれば捨てられる値なので、ディスクに残す理由がありません。redis-server --port 6379 --bind 127.0.0.1 --save '' --appendonly no --daemonize yesのように起動します。

レート制限サービスを起動する

/root/envd-rl/runtime/config/edge.yamlに、ドメインedgeのルールを2つ書いてください。ディスクリプターplan=freeは1時間に3回、plan=proは1時間に100回です。そしてイメージに入っているratelimit(envoyproxy/ratelimit)を起動してください。設定は環境変数で渡します: Redis127.0.0.1:6379、RUNTIME_ROOT=/root/envd-rl/runtime、RUNTIME_SUBDIRECTORY=.、RUNTIME_WATCH_ROOT=false、USE_STATSD=false、そしてHTTP8180・gRPC8181・デバッグ6070のポート(3つとも127.0.0.1)。

このサービスは、RUNTIME_ROOT/RUNTIME_SUBDIRECTORY/config/の下のYAMLを読みます。1つのファイルがドメイン1つで、ディスクリプターはkey・valueがリクエストが送ったものと正確に一致して初めて、ルールが掛かります。一致するルールがなければ、制限しません。正しく読み込まれたかは、デバッグポートのcurl localhost:6070/rlconfigが見せてくれます。edge.plan_free: unit=HOUR requests_per_unit=3のような行が出るはずです。起動するときは、env 변수=값 … setsid --fork nohup ratelimit > 로그 2>&1 </dev/null(プレースホルダーは環境変数名と値、ログファイルです)の形で書きます。

1台目のEnvoyがリクエストごとにディスクリプターを作って尋ねる

アップストリームpython3 /opt/lab/envoy/echo.py 8092を起動し、/root/envd-rl/envoy-a.yamlで管理ポート9983、リスナー127.0.0.1:10083のEnvoyを起動してください(--base-id 21)。HTTPフィルターは、local_ratelimit(stat_prefix local_rl) → ratelimit(ドメインedge、クラスターratelimitへgRPC、failure_mode_deny: false、enable_x_ratelimit_headers: DRAFT_VERSION_03) → routerの順です。ルートは2つあります。/localは、そのルートにだけローカルのトークンバケット(トークン2個、60秒ごとに2個)を設定し、/は、そのルートにrate_limitsで、リクエストヘッダーx-planをディスクリプターのキーplanにします。

グローバル制限フィルターは、自分では数えません。ルート(またはバーチャルホスト)のrate_limitsが、リクエストからディスクリプターを作り、フィルターは、それをサービスへ送るだけです。rate_limitsをバーチャルホストに置くと、そのホストのすべてのルートに掛かり、ルートに空のリスト(rate_limits: [])を書いてもオフになりません(実測)。そのため、/ルートに置きます。gRPCで尋ねるクラスターでは、HTTP/2を有効にする必要があります。x-planヘッダーのないリクエストは、ディスクリプターが作られないので、制限なしで通過します。

2台目のEnvoyも同じサービスを呼ぶ

/root/envd-rl/envoy-b.yamlに、1台目のEnvoyと同じで、管理ポート9984、リスナー127.0.0.1:10084の設定を書き、--base-id 22で起動してください。次に、2台のEnvoyにx-plan: proのリクエストを1つずつ送って、応答のx-ratelimit-remainingが続けて減るかを見て、2つの値を/root/envd-rl/04-remaining.txtにa=・b=の2行で書いてください(aを先に送ります)。

2台のEnvoyが別々に数えるなら、2つの応答のremainingが、同じ数から始まります。同じサービス・同じRedisのキーを上げるなら、2つ目が1つ目より1つ小さくなります。ヘッダー名は大文字小文字を区別しないので、curl -s -D - -o /dev/nullでヘッダーだけを受け取って探してください。2台のEnvoyを同じ--base-idで起動すると、2台目が共有メモリの名前が重なって、起動しません。

交互に送っても、上限は1つ

x-plan: freeのリクエスト8個を、A(10083)・B(10084)に交互に(Aから)/へ送り、順番に/root/envd-rl/05-global.txtに、a1=코드、b1=코드、a2=코드 … b4=코드の8行で書いてください(プレースホルダーはステータスコードです)。

freeは1時間に3回です。2台のEnvoyが上限を分け合うなら、8個のうち3個だけが200で、残りは429のはずです。どのEnvoyが受けたかとは無関係に。このウィンドウで、すでにfreeを送ったことがあるなら、200がより少ないかもしれませんが、それもグローバルに数えている証拠です。Redisでredis-cli --scan --pattern 'edge_plan_free_*'でキーを見て、getしてみると、2台のEnvoyのリクエストが、1つのキーに集まっています。

同じ8個をローカル制限で送ると

同じ方法で、今度は8個を/localへ送り、/root/envd-rl/06-local.txtにa1=코드 … b4=코드の8行で書いてください(プレースホルダーはステータスコードです)。

/localルートには、グローバル制限がなく、ローカルのトークンバケット(トークン2個)だけがあります。トークンバケットはEnvoy1台の中にあるので、Aにも2個、Bにも2個を与えます。グローバルの結果と並べると、プロキシを増やすときに何が変わるかが、数字で見えます。バケットは60秒ごとに満たされるので、もう一度試すときは、1分待ってください。

制限サービスが死ぬと、通過させる

ratelimitプロセスを止めたまま、Aにx-plan: freeのリクエストを1つ送り、そのステータスコードを/root/envd-rl/07-fail.txtにdown_code=で書いてください。その後、Aの統計cluster.echo.ratelimit.errorとcluster.echo.ratelimit.failure_mode_allowedの値を、error=・allowed=の2行で追記し、サービスを起動し直します。

freeはすでに上限を使い切っているので、サービスが生きていれば429です。サービスが死んだとき、failure_mode_deny: false(デフォルト値)は、リクエストを通過させます。レート制限は保護の仕組みなので、それが故障したからといって、サービス全体を止めはしない、という選択です。その代わり、その瞬間は統計にしか残りません。統計名のcluster.echoは、リクエストが向かっていたアップストリームのクラスター名です。

台数を増やすと上限がどうなるかを書く

/root/envd-rl/08-report.mdに、次の4行を書き、その下に学んだことを4行以上書いてください。global_ok_total=(ステップ5の200の個数)、local_ok_total=(ステップ6の200の個数)、fail_open_code=(ステップ7のdown_code)、counted_in=(グローバル制限で実際に数を数えた場所: envoyまたはredis)です。

値は前のステップのファイルから移してください。説明の行には、「プロキシを3台に増やすと、ローカル制限とグローバル制限の実際の上限が、それぞれどうなるか」と「2つを一緒に置くと何がよいか(ローカルは、制限サービスを守る最初の壁)」を書いておいてください。