大規模なプロジェクトの場合は、通常、Cloudflare Rate Limiting や HAProxy などのツールを使用するのが最善です。これらは強力で信頼性が高く、面倒な作業を代わりに行ってくれます。
ただし、小規模なプロジェクトの場合、または物事がどのように機能するかを知りたい場合は、コード内に独自のレート リミッターを直接作成できます。なぜ?
このガイドを最後まで読むと、API が過剰になるのを防ぐために TypeScript で基本的なスロットラーを構築する方法がわかります。ここで取り上げる内容は次のとおりです:
このガイドは実践的な出発点となるように設計されており、不必要に複雑にすることなく基本を学びたい開発者に最適です。 しかし、本番環境に対応していません。
始める前に、Lucia のレート制限セクションを正しく評価したいと思います。
スロットラー クラスを定義しましょう:
export class Throttler { private storage = new Map(); constructor(private timeoutSeconds: number[]) {} }
Throttler コンストラクターは、タイムアウト期間のリスト (timeoutSeconds) を受け入れます。ユーザーがブロックされるたびに、このリストに基づいて期間が徐々に長くなります。最終的に、最後のタイムアウトに達すると、コールバックをトリガーしてユーザーの IP を永久に禁止することもできます。ただし、それはこのガイドの範囲を超えています。
これは、間隔の増加に対してユーザーをブロックするスロットラー インスタンスを作成する例です:
const throttler = new Throttler([1, 2, 4, 8, 16]);
このインスタンスは、初めてユーザーを 1 秒間ブロックします。 2 回目は 2 回目、というように。
IP アドレスとそれに対応するデータを保存するためにマップを使用します。マップは頻繁な追加と削除を効率的に処理できるため、理想的です。
プロのヒント: 頻繁に変更される動的データにはマップを使用します。静的で変化しないデータの場合は、オブジェクトの方が適しています。 (ウサギの穴 1)
エンドポイントがリクエストを受信すると、ユーザーの IP アドレスを抽出し、スロットラーに問い合わせてリクエストを許可するかどうかを判断します。
ケース A: 新規または非アクティブなユーザー
IP がスロットラーで見つからない場合、それはユーザーの最初のリクエストであるか、ユーザーが十分な期間非アクティブであったかのいずれかです。この場合:
ケース B: アクティブ ユーザー
IP が見つかった場合は、ユーザーが以前にリクエストを行ったことを意味します。ここ:
後者の場合、最後のブロックから十分な時間が経過したかどうかを確認する必要があります。インデックスのおかげで、どの timeoutSeconds を参照する必要があるかがわかります。そうでない場合は、単にリバウンドしてください。それ以外の場合は、タイムスタンプを更新します。
export class Throttler { // ... public consume(key: string): boolean { const counter = this.storage.get(key) ?? null; const now = Date.now(); // Case A if (counter === null) { // At next request, will be found. // The index 0 of [1, 2, 4, 8, 16] returns 1. // That's the amount of seconds it will have to wait. this.storage.set(key, { index: 0, updatedAt: now }); return true; // allowed } // Case B const timeoutMs = this.timeoutSeconds[counter.index] * 1000; const allowed = now - counter.updatedAt >= timeoutMs; if (!allowed) { return false; // denied } // Allow the call, but increment timeout for following requests. counter.updatedAt = now; counter.index = Math.min(counter.index 1, this.timeoutSeconds.length - 1); this.storage.set(key, counter); return true; // allowed } }
インデックスを更新するときは、timeoutSeconds の最後のインデックスに制限されます。これがないと、counter.index 1 がオーバーフローし、次に this.timeoutSeconds[counter.index] にアクセスすると実行時エラーが発生します。
この例では、スロットラーを使用してユーザーが API を呼び出す頻度を制限する方法を示します。ユーザーがあまりにも多くのリクエストを行うと、メイン ロジックが実行されずにエラーが発生します。
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]); export async function GET({ getClientAddress }) { const IP = getClientAddress(); if (!throttler.consume(IP)) { throw error(429, { message: 'Too Many Requests' }); } // Read from DB, call OpenAI - do the thing. return new Response(null, { status: 200 }); }
ログイン システムでレート制限を使用すると、次の問題が発生する可能性があります:
これを防ぐには、レート制限に IP の代わりにユーザーの一意の userID を使用します。また、不必要なブロックを避けるために、ログインに成功した後にスロットラーの状態をリセットする必要があります。
Throttler クラスにリセット メソッドを追加します:
export class Throttler { // ... public reset(key: string): void { this.storage.delete(key); } }
ログインに成功した後にそれを使用します:
const user = db.get(email); if (!throttler.consume(user.ID)) { throw error(429); } const validPassword = verifyPassword(user.password, providedPassword); if (!validPassword) { throw error(401); } throttler.reset(user.id); // Clear throttling for the user
スロットラーは IP とレート制限を追跡するため、不要になった IP レコードをいつどのように削除するかを考えることが重要です。クリーンアップ メカニズムがないと、スロットラーはレコードをメモリに保存し続けるため、データが増大するにつれてパフォーマンスの問題が発生する可能性があります。
これを防ぐために、一定期間非アクティブになった後に古いレコードを定期的に削除するクリーンアップ機能を実装できます。ここでは、スロットラーから古いエントリを削除する簡単なクリーンアップ メソッドを追加する方法の例を示します。
export class Throttler { // ... public cleanup(): void { const now = Date.now() // Capture the keys first to avoid issues during iteration (we use .delete) const keys = Array.from(this.storage.keys()) for (const key of keys) { const counter = this.storage.get(key) if (!counter) { // Skip if the counter is already deleted (handles concurrency issues) return } // If the IP is at the first timeout, remove it from storage if (counter.index == 0) { this.storage.delete(key) continue } // Otherwise, reduce the timeout index and update the timestamp counter.index -= 1 counter.updatedAt = now this.storage.set(key, counter) } } }
クリーンアップをスケジュールする非常に簡単な方法 (ただし、おそらく最良ではない) は setInterval:
を使用することです。
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]) const oneMinute = 60_000 setInterval(() => throttler.cleanup(), oneMinute)
このクリーンアップ メカニズムは、スロットラーが古いレコードを無期限に保持しないようにし、アプリケーションの効率を維持するのに役立ちます。このアプローチはシンプルで実装が簡単ですが、より複雑なユースケース (より高度なスケジューリングの使用や高い同時実行性の処理など) では、さらに改良する必要がある場合があります。
定期的なクリーンアップにより、メモリの肥大化を防ぎ、しばらくリクエストを試行していないユーザーが追跡されないようにすることができます。これは、レート制限システムをスケーラブルでリソース効率の高いものにするための第一歩です。
冒険心があれば、プロパティがどのように割り当てられ、それがどのように変化するかを読むことに興味があるかもしれません。また、モノモーフィズムで特に好まれるインライン キャッシュなどの VM の最適化についても考えてみましょう。楽しむ。 ↩
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3