コンテンツにスキップ

レートリミッターを構築する

Durable ObjectsとWorkersを使用してレートリミッターを構築します。

この例では、Durable ObjectsとWorkersを使用して、アプリケーションが依存するサードパーティAPIやコストがかかるサービスを保護するために使用できるレートリミッターを構築する方法を示します。

この例では、Durable Objectsを使用してレートリミッターの設計時に考慮すべきいくつかの決定についても説明します。

Workerは、上流リソースを保護するためにIPごとにRateLimiter Durable Objectを作成します。IPベースのレート制限は、特定のIPがRateLimiter Durable Objectインスタンスと同じ地理的エリアに留まるため、レイテンシに悪影響を与えることなく効果的です。さらに、各IPが独自のDurable Objectを持つため、スループットも向上します。

グローバルレートリミッターを実装する方が簡単に思えるかもしれませんが、const id = env.RATE_LIMITER.idFromName("global");、これは上流リソースへのリクエストレートに対してより良い保証を提供できます。しかし:

  • これにより、すべてのリクエストがグローバルに単一のDurable Objectにサブリクエストを行う必要があります。
  • グローバルレートリミッターを実装すると、Durable Objectと同じ場所にないリクエストに追加のレイテンシが加わり、グローバルスループットは単一のDurable Objectのスループットに制限されます。
  • すべてのリクエストが依存する単一のDurable Objectは、通常、アンチパターンと見なされます。Durable Objectsは、ユーザー、ルーム、サービス、および/またはグローバルな調整が必要なアプリケーションの特定のサブセットにスコープを持つときに最も効果的に機能します。

Durable Objectは、トークンバケットアルゴリズムを使用してレート制限を実装します。単純なアイデアは、各リクエストが完了するためにトークンを必要とし、トークンは1秒あたりの希望リクエスト数の逆数に従って補充されるというものです。例えば、1秒あたり1000リクエストのレート制限では、指定された容量制限まで毎ミリ秒ごとにトークンが補充されます(milliseconds_per_requestで指定)。

この例では、Durable ObjectのアラームAPIを使用して、Durable Objectを将来の特定の時刻に起こすようにスケジュールします。

  • アラームのスケジュールされた時間が来ると、alarm()ハンドラーメソッドが呼び出され、この場合、アラームは「バケット」にトークンを追加します。
  • トークンを一括で追加することによって実装が効率的になり(milliseconds_for_updatesで指定)、アラームハンドラーが毎ミリ秒ごとに呼び出されるのを防ぎます。Durable Objectsの頻繁な呼び出しは、呼び出しと持続時間の料金を高くします。

レートリミッターの最初の実装は以下の通りです:

import { DurableObject } from "cloudflare:workers";
// Worker
export default {
async fetch(request, env, _ctx) {
// クライアントのIPアドレスを特定する
const ip = request.headers.get("CF-Connecting-IP");
if (ip === null) {
return new Response("クライアントIPを特定できませんでした", { status: 400 });
}
// クライアントのIPアドレスに基づいてDurable Objectの識別子を取得する
const id = env.RATE_LIMITER.idFromName(ip);
try {
const stub = env.RATE_LIMITER.get(id);
const milliseconds_to_next_request = await stub.getMillisecondsToNextRequest();
if (milliseconds_to_next_request > 0) {
// 必要な時間だけスリープすることもできます
return new Response("レート制限を超えました", { status: 429 });
}
} catch (error) {
return new Response("レートリミッターに接続できませんでした", { status: 502 });
}
// TODO: 実装してください
return new Response("上流リソースを呼び出します...")
}
};
// Durable Object
export class RateLimiter extends DurableObject {
static milliseconds_per_request = 1;
static milliseconds_for_updates = 5000;
static capacity = 10000;
constructor(ctx, env) {
super(ctx, env);
this.tokens = RateLimiter.capacity;
}
async getMillisecondsToNextRequest() {
this.checkAndSetAlarm()
let milliseconds_to_next_request = RateLimiter.milliseconds_per_request;
if (this.tokens > 0) {
this.tokens -= 1;
milliseconds_to_next_request = 0;
}
return milliseconds_to_next_request;
}
async checkAndSetAlarm() {
let currentAlarm = await this.ctx.storage.getAlarm();
if (currentAlarm == null) {
this.ctx.storage.setAlarm(Date.now() +
RateLimiter.milliseconds_for_updates * RateLimiter.milliseconds_per_request);
}
}
async alarm() {
if (this.tokens < RateLimiter.capacity) {
this.tokens = Math.min(RateLimiter.capacity,
this.tokens + RateLimiter.milliseconds_for_updates);
this.checkAndSetAlarm()
}
}
}

トークンバケットアルゴリズムはレート制限を実装するために人気がありますが、Durable Objectの機能を使用するよりも簡単なアプローチもあります:

// Durable Object
export class RateLimiter extends DurableObject {
static milliseconds_per_request = 1;
static milliseconds_for_grace_period = 5000;
constructor(ctx, env) {
super(ctx, env);
this.nextAllowedTime = 0;
}
async getMillisecondsToNextRequest() {
const now = Date.now();
this.nextAllowedTime = Math.max(now, this.nextAllowedTime);
this.nextAllowedTime += RateLimiter.milliseconds_per_request;
const value = Math.max(0,
this.nextAllowedTime - now - RateLimiter.milliseconds_for_grace_period);
return value;
}
}

最後に、選択した名前空間とクラス名に基づいてDurable Objectのバインディングマイグレーションを含むようにwrangler.tomlファイルを構成します。

wrangler.toml
name = "my-counter"
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"
[[migrations]]
tag = "v1"
new_classes = ["RateLimiter"]

関連リソース