この記事では、送信する要求の数をレート制限するクライアント側 HTTP ハンドラーを作成する方法について説明します。
HttpClient が "www.example.com"
リソースにアクセスすることが表示されます。 リソースは、リソースに依存するアプリによって消費され、1 つのリソースに対して要求が多すぎると、 リソースの競合につながる可能性があります。 リソースの競合は、リソースが多すぎるアプリによって消費され、リソースが要求しているすべてのアプリにサービスを提供できない場合に発生します。 これにより、ユーザー エクスペリエンスが低下し、場合によってはサービス拒否 (DoS) 攻撃につながる可能性もあります。 DoS の詳細については、「 OWASP: サービス拒否」を参照してください。
レート制限とは何ですか?
レート制限は、リソースにアクセスできる量を制限する概念です。 たとえば、アプリがアクセスするデータベースは、1 分あたり 1,000 件の要求を安全に処理できますが、それ以上の処理ができない場合があります。 1 分ごとに 1,000 件の要求のみを許可し、それ以上の要求を拒否してデータベースにアクセスできるようにするレートリミッターをアプリに配置できます。 したがって、データベースをレート制限し、アプリが安全な数の要求を処理できるようにします。 これは分散システムの一般的なパターンであり、アプリの複数のインスタンスが実行されていて、それらがすべて同時にデータベースにアクセスしないようにする必要があります。 要求のフローを制御するための複数の異なるレート制限アルゴリズムがあります。
.NET でレート制限を使用するには、 System.Threading.RateLimiting NuGet パッケージを参照します。
DelegatingHandler
サブクラスを実装する
要求のフローを制御するには、カスタム DelegatingHandler サブクラスを実装します。 これは、サーバーに送信される前に要求をインターセプトして処理できる HttpMessageHandler の一種です。 また、応答が呼び出し元に返される前に、応答をインターセプトして処理することもできます。 この例では、1 つのリソースに送信できる要求の数を制限するカスタム DelegatingHandler
サブクラスを実装します。 次のカスタム ClientSideRateLimitedHandler
クラスについて考えてみましょう。
internal sealed class ClientSideRateLimitedHandler(
RateLimiter limiter)
: DelegatingHandler(new HttpClientHandler()), IAsyncDisposable
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
using RateLimitLease lease = await limiter.AcquireAsync(
permitCount: 1, cancellationToken);
if (lease.IsAcquired)
{
return await base.SendAsync(request, cancellationToken);
}
var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
if (lease.TryGetMetadata(
MetadataName.RetryAfter, out TimeSpan retryAfter))
{
response.Headers.Add(
"Retry-After",
((int)retryAfter.TotalSeconds).ToString(
NumberFormatInfo.InvariantInfo));
}
return response;
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await limiter.DisposeAsync().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
limiter.Dispose();
}
}
}
前述の C# コードでは、次のことが行われます。
-
DelegatingHandler
型を継承します。 - IAsyncDisposable インターフェイスを実装します。
- コンストラクターから割り当てられる
RateLimiter
フィールドを定義します。 - サーバーに送信される前に要求をインターセプトして処理するために、
SendAsync
メソッドをオーバーライドします。 -
DisposeAsync() メソッドをオーバーライドして、
RateLimiter
インスタンスを破棄します。
SendAsync
メソッドを少し詳しく見ると、次のようになります。
-
RateLimiter
からRateLimitLease
を取得するには、AcquireAsync
インスタンスに依存します。 -
lease.IsAcquired
プロパティがtrue
されると、要求がサーバーに送信されます。 - それ以外の場合、 HttpResponseMessage は
429
状態コードと共に返され、lease
にRetryAfter
値が含まれている場合、Retry-After
ヘッダーはその値に設定されます。
多数の同時要求をエミュレートする
このカスタム DelegatingHandler
サブクラスをテストに配置するには、多数の同時要求をエミュレートするコンソール アプリを作成します。 このProgram
クラスは、カスタム HttpClientを使用してClientSideRateLimitedHandler
を作成します。
var options = new TokenBucketRateLimiterOptions
{
TokenLimit = 8,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = true
};
// Create an HTTP client with the client-side rate limited handler.
using HttpClient client = new(
handler: new ClientSideRateLimitedHandler(
limiter: new TokenBucketRateLimiter(options)));
// Create 100 urls with a unique query string.
var oneHundredUrls = Enumerable.Range(0, 100).Select(
i => $"https://example.com?iteration={i:0#}");
// Flood the HTTP client with requests.
var floodOneThroughFortyNineTask = Parallel.ForEachAsync(
source: oneHundredUrls.Take(0..49),
body: (url, cancellationToken) => GetAsync(client, url, cancellationToken));
var floodFiftyThroughOneHundredTask = Parallel.ForEachAsync(
source: oneHundredUrls.Take(^50..),
body: (url, cancellationToken) => GetAsync(client, url, cancellationToken));
await Task.WhenAll(
floodOneThroughFortyNineTask,
floodFiftyThroughOneHundredTask);
static async ValueTask GetAsync(
HttpClient client, string url, CancellationToken cancellationToken)
{
using var response =
await client.GetAsync(url, cancellationToken);
Console.WriteLine(
$"URL: {url}, HTTP status code: {response.StatusCode} ({(int)response.StatusCode})");
}
上記のコンソール アプリでは、次の手順を実行します。
-
TokenBucketRateLimiterOptions
は、8
のトークン制限、OldestFirst
のキュー処理順序、キューの3
の制限、1
ミリ秒の補充期間、2
の期間ごとのトークン数、およびtrue
の自動補充値で構成されます。 -
HttpClient
で構成されたClientSideRateLimitedHandler
を使用してTokenBucketRateLimiter
が作成されます。 - 100 個の要求をエミュレートするために、 Enumerable.Range は一意のクエリ文字列パラメーターを持つ 100 個の URL を作成します。
- Task メソッドから 2 つのParallel.ForEachAsync オブジェクトが割り当てられ、URL が 2 つのグループに分割されます。
-
HttpClient
は各 URL にGET
要求を送信するために使用され、応答はコンソールに書き込まれます。 - Task.WhenAll は、両方のタスクが完了するまで待機します。
HttpClient
はClientSideRateLimitedHandler
で構成されているため、すべての要求でサーバー リソースに対して行われるわけではありません。 このアサーションは、コンソール アプリを実行してテストできます。 サーバーに送信される要求の総数のごく一部のみが表示され、残りは 429
の HTTP 状態コードで拒否されます。
options
の作成に使用するTokenBucketRateLimiter
オブジェクトを変更して、サーバーに送信される要求の数がどのように変化するかを確認してみてください。
次の出力例を考えてみましょう。
URL: https://example.com?iteration=06, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=60, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=55, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=59, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=57, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=11, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=63, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=13, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=62, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=65, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=64, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=67, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=14, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=68, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=16, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=69, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=70, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=71, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=17, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=18, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=72, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=73, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=74, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=19, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=75, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=76, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=79, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=77, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=21, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=78, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=81, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=22, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=80, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=20, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=82, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=83, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=23, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=84, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=24, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=85, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=86, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=25, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=87, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=26, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=88, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=89, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=27, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=90, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=28, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=91, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=94, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=29, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=93, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=96, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=92, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=95, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=31, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=30, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=97, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=98, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=99, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=32, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=33, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=34, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=35, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=36, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=37, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=38, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=39, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=40, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=41, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=42, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=43, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=44, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=45, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=46, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=47, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=48, HTTP status code: TooManyRequests (429)
URL: https://example.com?iteration=15, HTTP status code: OK (200)
URL: https://example.com?iteration=04, HTTP status code: OK (200)
URL: https://example.com?iteration=54, HTTP status code: OK (200)
URL: https://example.com?iteration=08, HTTP status code: OK (200)
URL: https://example.com?iteration=00, HTTP status code: OK (200)
URL: https://example.com?iteration=51, HTTP status code: OK (200)
URL: https://example.com?iteration=10, HTTP status code: OK (200)
URL: https://example.com?iteration=66, HTTP status code: OK (200)
URL: https://example.com?iteration=56, HTTP status code: OK (200)
URL: https://example.com?iteration=52, HTTP status code: OK (200)
URL: https://example.com?iteration=12, HTTP status code: OK (200)
URL: https://example.com?iteration=53, HTTP status code: OK (200)
URL: https://example.com?iteration=07, HTTP status code: OK (200)
URL: https://example.com?iteration=02, HTTP status code: OK (200)
URL: https://example.com?iteration=01, HTTP status code: OK (200)
URL: https://example.com?iteration=61, HTTP status code: OK (200)
URL: https://example.com?iteration=05, HTTP status code: OK (200)
URL: https://example.com?iteration=09, HTTP status code: OK (200)
URL: https://example.com?iteration=03, HTTP status code: OK (200)
URL: https://example.com?iteration=58, HTTP status code: OK (200)
URL: https://example.com?iteration=50, HTTP status code: OK (200)
最初に記録されたエントリは常にすぐに返される 429 応答であり、最後のエントリは常に 200 応答であることがわかります。 これは、レート制限がクライアント側で検出され、サーバーへの HTTP 呼び出しを回避するためです。 これは、サーバーに要求が殺到していないことを意味するため、良いことです。 また、すべてのクライアントでレート制限が一貫して適用されることを意味します。
各 URL のクエリ文字列は一意であることにも注意してください。 iteration
パラメーターを調べて、要求ごとに 1 ずつインクリメントされることを確認します。 このパラメーターは、429 応答が最初の要求ではなく、レート制限に達した後に行われた要求からの応答であることを示すのに役立ちます。 200 件の応答は後で到着しますが、これらの要求は、制限に達する前に以前に行われました。
さまざまなレート制限アルゴリズムについて理解を深めるために、別の RateLimiter 実装を受け入れるようにこのコードを書き直してみてください。 TokenBucketRateLimiterに加えて、次の方法を試すことができます。
概要
この記事では、カスタム ClientSideRateLimitedHandler
を実装する方法について説明しました。 このパターンを使用して、API の制限があることがわかっているリソースのレート制限付き HTTP クライアントを実装できます。 この方法では、クライアント アプリがサーバーに不要な要求を行うことを防ぎ、サーバーによってアプリがブロックされるのを防いでいます。 さらに、メタデータを使用して再試行のタイミング値を格納する場合は、自動再試行ロジックを実装することもできます。
こちらも参照ください
.NET