APIレート制限:アルゴリズムと実装
あなたのAPIが攻撃を受けています。高度なハッカーによるものではなく、毎秒何千回もエンドポイントを叩き、リソースを消費し、正規ユーザーのサービスを低下させる単純なスクリプトによるものです。レート制限がなければ、1つの問題のあるクライアントがアプリケーション全体をダウンさせる可能性があります。このガイドでは、コアアルゴリズムを説明し、Node.jsとNginxでの実用的な実装を示してAPIを保護します。
レート制限が重要な理由
レート制限は、クライアントが一定の時間枠内で行えるリクエスト数を制御します。これは以下に対する最初の防御線です:
- ログインやAPIキーエンドポイントへのブルートフォース攻撃
- 過剰なリクエストによるサービス拒否(DoS)
- データベースクエリやファイル処理などの高コスト操作によるリソース枯渇
- スクレイパーやボットによる無料枠の悪用
セキュリティ以外にも、レート制限は公平な使用を保証し、段階的な価格プランなどのビジネスルールを適用するのに役立ちます。
レート制限アルゴリズム
APIレート制限では4つのアルゴリズムが主流です。それぞれ精度、メモリ使用量、バースト処理にトレードオフがあります。
1. 固定ウィンドウ
固定された時間間隔(例:1分あたり100リクエスト)でリクエストをカウントします。ウィンドウがリセットされるとカウンタもリセットされます。
- 長所: シンプル、低メモリ(クライアントごとに1つのカウンタ)。
- 短所: ウィンドウ境界でのバースト。クライアントは12:00:59に100リクエストを送信し、12:01:00にさらに100リクエストを送信でき、実質2秒で200リクエストになります。
2. スライディングウィンドウ
各リクエストのタイムスタンプを追跡し、直近N秒以内にいくつあるかをカウントします。これによりバーストが平滑化されます。
- 長所: 正確、境界でのスパイクなし。
- 短所: メモリ使用量が高い(タイムスタンプを保存)か、加重平均を用いたスライディングウィンドウカウンタを使用。
3. トークンバケット
バケットにトークンが入っています。トークンは固定レートで追加されます。各リクエストは1つのトークンを消費します。バケットが空の場合、リクエストは拒否されます。
- 長所: バケットサイズまでのバーストを許可し、その後平均レートを強制。
- 短所: クライアントごとにトークン数と最終補充時刻を保存する必要がある。
4. リーキーバケット
リクエストはキュー(バケット)に入り、一定レートで処理されます。キューが満杯の場合、リクエストは破棄されます。
- 長所: トラフィックを安定したレートに平滑化、下流サービスを保護するのに理想的。
- 短所: レイテンシが増加;リアルタイムAPIには不向き。
| アルゴリズム | バースト処理 | メモリ | ユースケース |
|---|---|---|---|
| 固定ウィンドウ | 不良 | 低 | シンプルなAPI |
| スライディングウィンドウ | 良好 | 中 | 汎用 |
| トークンバケット | 優秀 | 中 | バースト許容のあるAPI |
| リーキーバケット | なし | 中 | トラフィックの平滑化 |
Node.jsでのレート制限の実装
ExpressとRedisを使用して、分散状態のためのトークンバケットレートリミッタを実装します。複数のサーバーインスタンスがある場合、Redisは不可欠です。
ステップ1:依存関係のインストール
npm install express redis
ステップ2:レートリミッタミドルウェアの作成
const redis = require('redis');
const client = redis.createClient();
async function tokenBucketLimiter(req, res, next) {
const key = `rate_limit:${req.ip}`;
const capacity = 10; // max tokens
const refillRate = 1; // tokens per second
const now = Date.now();
const data = await client.hGetAll(key);
let tokens = data.tokens ? parseFloat(data.tokens) : capacity;
let lastRefill = data.lastRefill ? parseInt(data.lastRefill) : now;
// Refill tokens based on elapsed time
const elapsed = (now - lastRefill) / 1000;
tokens = Math.min(capacity, tokens + elapsed * refillRate);
if (tokens < 1) {
return res.status(429).json({ error: 'Too many requests' });
}
tokens -= 1;
await client.hSet(key, {
tokens: tokens.toString(),
lastRefill: now.toString()
});
await client.expire(key, 60); // auto-cleanup
next();
}
app.use(tokenBucketLimiter);
このミドルウェアはトークン数をアトミックにチェックして更新します。本番環境では、競合状態を避けるためにRedisトランザクションまたはLuaスクリプトを使用してください。
Nginxでのレート制限の実装
Nginxはlimit_reqモジュールによる組み込みのレート制限を提供します。これはリーキーバケットアルゴリズムを使用します。
ステップ1:レート制限ゾーンの定義
nginx.confのhttpブロック内:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
これにより、IPごとに毎秒10リクエストを許可する10MBのゾーンapiが作成されます。
ステップ2:制限の適用
locationブロック内:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
burst=20は最大20リクエストの短いバーストを許可します。nodelayはバーストリクエストをキューに入れずに即座に処理します。
APIレート制限のベストプラクティス
- 適切なステータスコードを返す:
429 Too Many Requestsを使用し、Retry-Afterヘッダーを含めます。 - クライアントを正しく識別する: IPアドレスは共有(NAT)または偽装される可能性があるため、可能な限りAPIキーやユーザーIDを使用します。
- 状態を分散する: マルチインスタンス展開にはRedisまたは類似のストアを使用します。
- ログと監視: レート制限のヒットを追跡して攻撃を検出し、しきい値を調整します。Nginx Log Analyzerのようなツールは、429レスポンスを分析し、悪用IPを特定するのに役立ちます。
- 制限を伝える: APIドキュメントにレート制限を記載し、
X-RateLimit-Limit、X-RateLimit-Remainingなどのヘッダーを含めます。
FAQ
レート制限とスロットリングの違いは何ですか?
レート制限はしきい値を超えたリクエストをブロックしますが、スロットリングはそれらを遅くします(例:キューに入れるか遅延させる)。レート制限は二値的で、スロットリングは段階的です。
どのレート制限アルゴリズムを使用すべきですか?
ほとんどのAPIでは、トークンバケットが良いバランスを提供します:バーストを許可しつつ平均レートを強制します。厳密な平滑化が必要な場合はリーキーバケットを使用してください。シンプルさを求めるなら、固定ウィンドウは低トラフィックのAPIに適しています。
認証済みユーザーと匿名ユーザーのレート制限をどう扱いますか?
匿名ユーザーにはより厳しい制限(例:IPごと)を適用し、認証済みユーザーにはより寛大な制限(例:APIキーごと)を適用します。サブスクリプションプランに基づく段階的な制限を実装することもできます。
APIトラフィックを分析する準備はできましたか?Nginx Log Analyzerを使用してログを解析し、レート制限違反を発見し、しきい値を最適化しましょう。