プロキシを増やしたら上限も増えた
一言でいうと
グローバルレート制限は、数える場所をプロキシの外の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つの上限を分け合うことと、同じリクエストをローカルのトークンバケットへ送ると、台ごとに別々に数えられることを、並べて数えます。最後にサービスを止めて、デフォルトが通過であることを、応答と統計で確認します。