本文介绍如何创建客户端 HTTP 处理程序,以限制发送的请求数。 你将看到访问 HttpClient 资源的 "www.example.com"
。 资源由依赖它们的应用使用,当应用对单个资源发出过多请求时,可能会导致 资源争用。 资源争用发生在资源被过多应用程序消耗时,导致资源无法服务于所有请求它的应用。 这可能会导致用户体验不佳,在某些情况下,它甚至可能导致拒绝服务(DoS)攻击。 有关 DoS 的详细信息,请参阅 OWASP:拒绝服务。
什么是速率限制?
速率限制是限制可以访问的资源量的概念。 例如,你可能知道应用访问的数据库可以安全地处理每分钟 1,000 个请求,但处理次数可能不多于此。 可以在应用中放置速率限制器,该限制器每分钟只允许 1,000 个请求,并拒绝任何其他请求,然后才能访问数据库。 因此,对数据库进行速率限制,并允许应用处理合理数量的请求。 这是分布式系统中的一种常见模式,其中你可能有多个应用实例正在运行,并且你希望确保它们不会同时尝试访问数据库。 有多个不同的速率限制算法来控制请求流。
若要在 .NET 中使用速率限制,请引用 System.Threading.RateLimiting NuGet 包。
实现DelegatingHandler
子类
若要控制请求流,请实现自定义 DelegatingHandler 子类。 这是一种类型 HttpMessageHandler ,可用于在请求发送到服务器之前截获和处理请求。 还可以在响应返回到调用方之前截获和处理响应。 在此示例中,你将实现一个自定义 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,每个 URL 都具有唯一的查询字符串参数。
- 从Task方法中分配两个Parallel.ForEachAsync对象,用来将 URL 拆分为两个组。
-
HttpClient
用于向每个 URL 发送GET
请求,并将响应写入控制台。 - Task.WhenAll 等待两个任务完成。
由于 HttpClient
配置了 ClientSideRateLimitedHandler
,因此并非所有请求都会到达服务器资源。 可以通过运行控制台应用来测试此断言。 你会看到,总请求数中只有一小部分被发送到服务器,其余的请求因 HTTP 状态代码 429
而被拒绝。 尝试更改 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
参数,可以看到该参数在每次请求时增加一个。 此参数有助于说明 429 个响应不是来自第一个请求,而是来自达到速率限制后发出的请求。 200 个响应稍后会到达,但这些请求是在达到限制之前发出的。
若要更好地了解各种速率限制算法,请尝试重写此代码以接受不同的 RateLimiter 实现。 除了 TokenBucketRateLimiter 你可以尝试:
概要
本文介绍了如何实现自定义 ClientSideRateLimitedHandler
。 此模式可用于为已知具有 API 限制的资源实现速率限制的 HTTP 客户端。 这样,您可以防止客户端应用向服务器发出不必要的请求,也可以避免您的应用被服务器阻止。 此外,使用元数据来存储重试计时值,还可以实现自动重试逻辑。