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

Envoyの内部構造

プロキシを増やしたら上限も増えた

TT Labで続きを見る

一言でいうと

グローバルレート制限は、数える場所をプロキシの外の1か所に置きます。Envoyはリクエストごとにディスクリプター(descriptor)を作り、レート制限サービスにgRPCで尋ね、サービスはRedisのカウンターを上げて、許可か超過かを答えます。そのため、プロキシが何台あっても、上限は1つです。

なぜ必要なのか

ローカルレート制限(トークンバケット)は、高速で依存関係がありません。その代わり、バケットがEnvoy1台の中にあります。公式ドキュメントも、ローカル制限のバケットは、デフォルトで「Envoyプロセス1つ」に掛かると明記しています。イングレスプロキシを2台から6台に増やした瞬間、「顧客1人あたり1時間に1,000回」という約束が、6,000回になります。オートスケーリングが付けば、上限がトラフィックに合わせて勝手に増えます。防ごうとした急増が来るほど、門が広がるようなものです。

API販売のように、全体にかかる約束があるなら、数える場所は1つでなければなりません。それがグローバルレート制限サービスで、Envoy側には、そのサービスに尋ねるHTTPフィルター(envoy.filters.http.ratelimit)があります。サービス側のリファレンス実装が、envoyproxy/ratelimitです。

どう動くのか

1. ルートがディスクリプターを作ります。フィルターは、自分では何を数えるかを知りません。ルート(またはバーチャルホスト)のrate_limitsが、リクエストから値を取り出して、ディスクリプターを作ります。

rate_limits:
  - actions:
      - request_headers: { header_name: x-plan, descriptor_key: plan }
# x-plan: free 인 요청 → 설명자 [("plan", "free")]

ヘッダーがなければ、ディスクリプターが作られず、そのリクエストは制限を受けません。バーチャルホストに書いたrate_limitsは、そのホストのすべてのルートに掛かり、ルートに自分のrate_limitsがあるときだけ、代わりにそれが使われます。

2. サービスがルールと照合します。サービスの設定は、ドメイン1つにディスクリプタールールのリストです。

設定 意味
domain: edge Envoyフィルターのdomainと同じでないと、このルールを使いません
key: plan、value: free ディスクリプターがちょうどこの組のとき
unit: hour、requests_per_unit: 3 時間ウィンドウごとに3回

3. Redisが数えます。サービスは、ウィンドウごとにキー1つ(edge_plan_free_<창 시작 시각>のプレースホルダーはウィンドウの開始時刻です)をINCRBYで上げ、ウィンドウの長さだけTTLを設定します。サービス自体は状態を持たないので、複数台起動してもかまいません。上限は、Redisのキー1つに集まります。

4. 結果が応答に現れます。超過なら、Envoyが429を返します。enable_x_ratelimit_headersを有効にすると、x-ratelimit-limit・x-ratelimit-remaining・x-ratelimit-resetヘッダーが付き、クライアントがあとどれだけ残っているかがわかります。

サービスが死ぬと、failure_mode_denyが決めます。デフォルト値のfalseは通過です。レート制限は保護の仕組みなので、それが故障したからといってサービスを止めはしない、という選択です。その代わり、その瞬間は、統計cluster.<업스트림>.ratelimit.error・failure_mode_allowed(プレースホルダーはアップストリーム名です)にしか残りません。

2つを一緒に使います。グローバル制限は、リクエストごとにネットワークの往復が1回増え、そのサービスが新しいボトルネックになります。そのため、ローカル制限を前に置いて急増を先に弾き、グローバル制限は約束を守るために使います。公式ドキュメントが勧める組み合わせも、これです。

現場での姿

「プロキシを増やしたら、顧客の上限が増えました」という場合です。ローカル制限だけを使った場合です。上限を台数で割って書いておく方法は、オートスケーリングの前で崩れます。

「上限の設定を変えたのに、適用されません」という場合です。ディスクリプターがルールと1文字でも違えば、ルールが掛からず、掛からなかったリクエストは制限なしで通過します。サービスの/rlconfigでルールが読み込まれたか、Envoyがどんなディスクリプターを送っているかを、分けて確認します。

「Redisの障害の間、誰もブロックされませんでした」という場合です。デフォルトが通過だからです。これが意図なら統計にアラートを設定し、意図でなければfailure_mode_deny: trueに変えますが、そうするとRedisがすべてのリクエストの可用性を握ることも、一緒に受け入れる必要があります。

公式ドキュメント: Global rate limiting・Rate limit filter・Local rate limit filter・envoyproxy/ratelimit

次のラボですること

Redisとリファレンス実装のレート制限サービスをPodの中に起動し、同じサービスを呼ぶEnvoyを2台立てます。2台に交互に送ったリクエストが、1つの上限を分け合うことと、同じリクエストをローカルのトークンバケットへ送ると、台ごとに別々に数えられることを、並べて数えます。最後にサービスを止めて、デフォルトが通過であることを、応答と統計で確認します。