レートリミッターのシステム設計:アルゴリズムとアーキテクチャ図

分散レートリミッターを設計します。トークンバケットとスライディングウィンドウ、配置場所、Redis のカウンター、429 応答、Redis 障害時の動き。

· 創業者・開発者

公開日:

レートリミッターのシステム設計:アルゴリズムとアーキテクチャ図

公開 API には必ずレートリミッターが必要で、ほとんどのチームは同じ形でそれに気づきます。リトライループを持つクライアント 1 つ、あるいはスクレイパー 1 つが、ほかの全員のためのサービスを落としてしまうのです。リミッターは、1 つのキー、ユーザー、IP が要求できる量に上限を設けます。そうすれば、1 人の迷惑な利用者のコストは障害ではなく 429 で済みます。

設計上の問いはいつも同じです。どのアルゴリズムを使うか、リミッターをどこに置くか、カウンターをどこに保存するか、そしてカウンターのストアが落ちたらどうなるか。この記事ではそれぞれに答え、最後に API の前に置く分散リミッターのアーキテクチャ図を示します。

リミッターがやるべきこと

  • リクエストのたびに、許可するかどうかを数ミリ秒で判断する。
  • 呼び出し元ごとに異なるルールを適用する。API キーごと、ユーザーごと、IP ごと、ルートごと。
  • どのサーバーインスタンスがリクエストを処理しても同じように振る舞う。
  • 制限されたこと、いつ再試行すべきかをクライアントにはっきり伝える。
  • 自身のストレージに問題が起きても動き続けるか、予測できる形で失敗する。

これを分散システムの問題にしているのは 3 つ目の点です。10 台のゲートウェイインスタンスがそれぞれ自分の 100 を数えるなら、「1 分あたり 100 リクエスト」という上限には意味がありません。

5 つのアルゴリズム

アルゴリズム 数え方 強み 弱み
トークンバケット バケットが一定の速度で補充され、リクエストごとにトークンを 1 つ消費する バケットのサイズまで短いバーストを許容する 調整するパラメーターが 2 つある
リーキーバケット リクエストがキューから一定の速度で流れ出る 出力の速度が滑らか バーストはキューで待たされ、レイテンシーが増える
固定ウィンドウカウンター キーごと・1 分ごとに 1 つのカウンター 最も安い。インクリメント 1 回で済む ウィンドウの境目で上限の 2 倍が通り得る
スライディングウィンドウログ リクエストごとにタイムスタンプを保存する 正確 メモリがトラフィックに比例して増える
スライディングウィンドウカウンター 前のウィンドウの件数を重なりの割合で重み付けする ほぼ正確で、キーごとのカウンターは 2 つ 近似にすぎない

固定ウィンドウの問題は簡単にわかります。クライアントが 12:00:59 に 100 件、12:01:00 にさらに 100 件送ると、両方のウィンドウがそれを受け入れます。1 分あたり 100 件の上限に対して、2 秒で 200 件です。

API の既定はたいていトークンバケットです。バーストはクライアントの普通の振る舞いだからです。「1 分あたり 100 リクエスト、バースト 20」のような上限は、そのまま補充速度とバケットサイズに対応します。Stripe は レートリミッターで API をスケールさせる記事 で、Redis 上でトークンバケットのリミッターを動かしていること、そして同時リクエスト数を制限する別のリミッターも併用していることを説明しています。

ウィンドウごとの厳密な上限がほしい場合、境目の問題を安く取り除く方法がスライディングウィンドウカウンターです。

リミッターの配置場所

上限は 3 か所で強制でき、多くのシステムは複数を併用します。

  • エッジ(CDN や WAF)。 IP ごとの大まかな上限で、大量のリクエストが届く前に吸収します。
  • API ゲートウェイ。 呼び出し元が誰かを知る必要がある、キーごと・ルートごとの上限です。製品としての上限の多くはここにあります。
  • サービスの内部。 「アカウントごとに同時エクスポートは 2 件まで」のように、業務データに依存する上限です。

ゲートウェイはローカルに数える(インスタンスごとにカウンターを持つ)こともできますし、グローバルに数える(全インスタンスがストアを共有する)こともできます。ローカルの上限は無料で、大まかな保護には十分です。上限をインスタンス数で割り、誤差を受け入れます。顧客がお金を払う製品の上限には、グローバルなストアが必要です。たとえば Envoy は両方に対応しています。ローカルのレート制限フィルターと、Redis を使う外部のレート制限サービスを呼び出すグローバルモードです。

Redis のカウンター

グローバルなカウンターには Redis がよく使われます。高速で、カウンターをメモリに保持し、キーを自動で期限切れにできるからです。落とし穴は、カウンターを読んでから書き戻すまでの競合です。2 つのゲートウェイが 99 を読み、どちらもリクエストを許可し、どちらも 100 を書き込みます。

判断は必ずアトミックにします。固定ウィンドウなら、新しいキーに対して INCR のあとに EXPIRE を実行すれば十分です。Redis の INCR ドキュメント には、これを使ったレート制限のパターンが載っています。トークンバケットなら、バケットを読み、経過時間に応じて補充し、トークンを 1 つ取り、書き戻すまでを 1 ステップで行う小さな Lua スクリプトを実行します。Redis はスクリプトの実行中にほかのコマンドを割り込ませないので、同時のリクエストでも一貫したバケットが見えます。

-- KEYS[1] = bucket key, ARGV = rate per second, capacity, now (ms), cost
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local rate, cap, now, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
local tokens = tonumber(b[1]) or cap
local ts = tonumber(b[2]) or now
tokens = math.min(cap, tokens + (now - ts) / 1000 * rate)
local allowed = tokens >= cost
if allowed then tokens = tokens - cost end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate * 1000))
return allowed and 1 or 0

時刻は呼び出し元から渡すか、Redis の TIME コマンドを使います。多数のゲートウェイの時計が混ざると、おかしな結果が出る原因になりがちです。同じ Redis をキャッシュとしても使っているなら、退避ポリシーに注意してください。退避されたバケットは、リセットされた上限と同じです。退避の設定は キャッシュ戦略の記事 で扱っています。

クライアントへの伝え方

制限したリクエストには、RFC 6585 で定義された 429 Too Many Requests で応答し、待つべき秒数を入れた Retry-After ヘッダーを付けます。行儀のよいクライアントは待ってくれますし、そうでないクライアントにも少なくとも明確なシグナルが届きます。IETF のドラフト「RateLimit header fields for HTTP」は、上限に達する前にクォータと残りの予算をクライアントに伝える標準ヘッダーを提案しています。執筆時点(2026 年 10 月)ではまだドラフトなので、そこにあるヘッダー名は暫定的なものとして扱ってください。

上限はドキュメントに書きます。ブロックされて初めてクライアントが気づく上限は、サポートチケットに変わります。

Redis が落ちたとき

起きる前に決めておきます。

  • フェイルオープン。 リミッターが判断できないときはリクエストを通します。API は動き続け、その間は保護を失います。製品の上限では普通こちらを選びます。
  • フェイルクローズ。 リクエストを拒否します。ログイン試行や SMS 送信のように、コストが高いか悪用されやすい操作に向いています。
  • ローカルの上限にフォールバックする。 ストアが戻るまで、各ゲートウェイが大まかなローカル上限を適用します。

どれを選んでも、アラートを設定します。1 週間黙ってフェイルオープンしていたリミッターは、リミッターとは呼べません。

アーキテクチャ図

API クライアントはロードバランサーと API ゲートウェイに到達します。ゲートウェイはレート制限サービスに問い合わせ、サービスはアトミックな Lua スクリプトで Redis のカウンターを更新します。判断ノードは上限内のリクエストを Orders API へ、それ以外を Retry-After 付きの 429 へ送ります。件数はメトリクスへ送られます。

10 個のコンポーネント · 8 個の接続

Archboard で開く
ダイアグラムの内容
  • API clients
  • Load balancer
  • API gateway
  • Rate limit service
  • Redis: counters
  • Under the limit?
  • Orders API
  • 429 + Retry-After
  • Metrics
  • key + route
  • atomic Lua script
  • yes
  • no
  • allowed vs rejected
API ゲートウェイの背後に置いた分散レートリミッター。1 つのサービスがルールを適用し、アトミックなスクリプトで Redis のカウンターを管理し、上限を超えたクライアントには Retry-After 付きの 429 を返し、許可数と拒否数を報告します。

図の読み方は次のとおりです。

  • 左から右 がリクエストの経路です。クライアント、ロードバランサー、API ゲートウェイ、レート制限サービスと続きます。
  • レート制限サービス はルールを持ち、リクエストごとに 1 つの判断を下します。カウンターを Redis に保持し、アトミックな Lua スクリプトで更新します。
  • 判断ノード は許可されたリクエストを Orders API へ送ります。それ以外は Retry-After 付きの 429 を受け取り、バックエンドには届きません。
  • メトリクス は非同期に報告されるので破線です。顧客がブロックされたと言ってきたら、まずルールごとの拒否数を確認します。
  • Markdown ブロック には、障害時の挙動も含めて、ルールを確認される順に並べています。

図に載せるべきこと

  • ルールとそのキー。 「100/分」は、「API キーごと」か「IP ごと」かがなければ意味がありません。
  • ローカルかグローバルか。 その違いで、上限がインスタンスをまたいで守られるかが決まります。
  • 障害時の挙動。 フェイルオープンかフェイルクローズかは、コードだけでなく図にも書きます。
  • 拒否の経路。 429 をどこで返すかを示し、バックエンドがそのリクエストを見ていると誤解されないようにします。

ゲートウェイそのものについては、API ゲートウェイのアーキテクチャ図 でルーティング、認証、リミッターと並んで動くほかのフィルターを扱っています。システム設計図の例 では、リミッターがより大きな設計にどう収まるかを示しています。

Archboard で作る

  1. Google または GitHub でサインインし、図を作成します。
  2. クライアント用に C4 モデルパックから 人物 を追加し、続いてクラウドパックから クラウドロードバランサー と APIゲートウェイ を追加します。
  3. C4 モデルパックから コンテナ ノードを 2 つ追加します。レート制限サービスと Orders API です。レート制限サービスの下にデータパックの Redis を置きます。
  4. 許可か拒否かの分岐用にフローチャートパックから 判断 と 開始 / 終了 を、DevOps パックから メトリクス を追加します。
  5. エッジにラベルを付け、メトリクスのエッジは破線にして、ルールを書いた基本パックの Markdown ブロックを追加します。

この例で使うダイアグラムノードは 10 個で、Free プランの範囲に収まります。

よくある質問

どのレート制限アルゴリズムを使えばよいですか? ほとんどの API ではトークンバケットです。実際のクライアントが生む短いバーストを許容できるからです。リクエストログ全体を持つコストをかけずにウィンドウごとの厳密な上限がほしいなら、スライディングウィンドウカウンターを使います。

レートリミッターは独立したサービスにすべきですか? 必ずしもそうではありません。共有の Redis を使うゲートウェイのフィルターで、多くのシステムには十分です。多数のゲートウェイやサービスが同じルールを共有するなら、独立したサービスが役立ちます。

制限したリクエストにはどのステータスコードを返すべきですか? Retry-After ヘッダー付きの 429 Too Many Requests です。503 は使わないでください。要求が多すぎるのではなく、サービスが落ちているとクライアントに伝えてしまいます。

リクエストが多数のサーバーを経由する場合、ユーザーごとに制限するには? カウンターを Redis のような共有ストアに、ユーザーや API キーをキーにして保持し、アトミックに更新します。サーバーごとのローカルなカウンターは、大まかな上限としてしか機能しません。

レートリミッター自体が故障したらどうなりますか? あらかじめ決めておきます。通常の製品の上限はフェイルオープン、コストが高いか悪用されやすい操作はフェイルクローズにし、どちらの場合もアラートを出します。

無料で始める — 1 シート、クレジットカード不要。

関連記事

← ブログに戻る

1分でキャンバスを開く

Google または GitHub でサインイン。1シートは無料で、チームが加わったらアップグレード。