System.Threading.RateLimiting 在 .NET 7 發佈,他提供了 4 種的限速方式,當需要限制執行速度時,透過它讓我們可以根據需求來決定 Web API、HttpClient、流程限速,有了這個就可以不用自己控制執行速度了
開發環境
- Windows 11
- JetBrains Rider 2023.3.3
- .NET 8
- System.Threading.RateLimiting 8.0.0
4 種限速器
- FixedWindowRateLimiter:(固定視窗限制) 允許限制,例如“每分鐘 60 個請求”。每分鐘可以發出 60 個請求,即每秒一個,也可以一次性發出 60 個。
- SlidingWindowRateLimiter:(滑動視窗限制)與固定視窗限制類似,但使用分段來實現更細粒度的限制。
- TokenBucketRateLimiter:(令牌桶限制)允許控制流速,並允許突發。“您每分鐘收到 100 個請求”。如果您在 10 秒內完成所有請求,則必須等待 1 分鐘才能允許更多請求。
- ConcurrencyLimiter:(併發限制)是速率限制的最簡單形式。它不考慮時間,只考慮併發請求的數量。例如,“允許 10 個併發請求”。
這裡有更詳細的解釋,可以參考,理解 ASP.NET Core - 限流(Rate Limiting) - xiaoxiaotank - 博客园 (cnblogs.com)
RateLimiter
所有的限速器都是實作 RateLimiter 類別 (原始碼),
- 先呼叫 AcquireAsync(非同步) 或 AttemptAcquire(同步) 這兩個方法來取得 RateLimitLease
- 在判斷 RateLimitLease.IsAcquired 有沒有額度,沒有的話則等待、重試
RateLimiter limiter = GetLimiter();
using RateLimitLease lease = limiter.AttemptAcquire(permitCount: 1);
if (lease.IsAcquired)
{
// Do action that is protected by limiter
}
else
{
// Error handling or add retry logic
}
範例
接下來,來看幾個範例
Client 端使用限速器實現流程限速
假如我有一個流程,裡面呼叫了很多 Web API,伺服器也沒有控制限速,為了避免伺服器被我打趴,這時候我需要對整個流程限制速度,這裡我使用 FixedWindowRateLimiter。
FixedWindowRateLimiterOptions 的配置如下
- PermitLimit = 10,每次處理
- Window = 10,
- AutoReplenishment,自動補額度
- QueueProcessingOrder = QueueProcessingOrder.OldestFirst,舊的先處理
- QueueLimit = 1,指定 QueueLimit 在到達 時 PermitLimit 將排隊但不拒絕的傳入請求數。
每 10 秒視窗最多允許 10 個請求,若發生更多的請求,最多 1 個請求加入排隊,等待下一次被處理
using System.Threading.RateLimiting;
using Lab.HttpClientLimit;
var fixedWindowRateLimiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromSeconds(10),
AutoReplenishment = true,
PermitLimit = 10,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1
});
var count = 0;
while (true)
{
var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: default);
if (!lease.IsAcquired)
{
Console.WriteLine("Rate limit exceeded. Pausing requests for 1 minute.");
await Task.Delay(TimeSpan.FromMinutes(1));
continue;
}
//模擬 API
var tasks = new List<Task>()
{
Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(100)); }),
Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(120)); }),
};
await Task.WhenAll(tasks);
count++;
Console.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss}, Run Count: {count}");
}
執行結果,每 10 秒執行 10 次,如下圖:
使用限速器實現 HttpClient 請求限制
除了流程可以限速,也可以在 HttpClient 加上 RateLimit DelegatingHandler 來限制發送請求,實作 DelegatingHandler 並傳入 RateLimiter
internal sealed class ClientSideRateLimitedHandler(RateLimiter limiter)
: DelegatingHandler(new HttpClientHandler()), IAsyncDisposable
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
using var 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();
}
}
}
HttpClient 傳入 ClientSideRateLimitedHandler,
var limiter = fixedWindowRateLimiter;
using HttpClient client = new(
handler: new ClientSideRateLimitedHandler(limiter: limiter));
調用端模擬同時送出多個請求
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})");
}
執行結果,只有 10 個請求成功,其餘的都被拒絕發送,如下圖
參考
Rate limiting an HTTP handler in .NET - .NET | Microsoft Learn
ASP.NET Core 中的速率限制中介軟體 | Microsoft Learn
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET