通過 RateLimiter 限速器,限制執行速度

System.Threading.RateLimiting 在 .NET 7 發佈,他提供了 4 種的限速方式,當需要限制執行速度時,透過它讓我們可以根據需求來決定 Web API、HttpClient、流程限速,有了這個就可以不用自己控制執行速度了

Token Bucket Rate Limiter. Image 1 of 4


 

 開發環境

  • 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 類別 (原始碼),

  1. 先呼叫 AcquireAsync(非同步) 或 AttemptAcquire(同步)  這兩個方法來取得 RateLimitLease
  2. 在判斷 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

範例位置

sample.dotblog/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit at da7e7a0f0072505b97a8aa8e42028ec4ac3574a9 · yaochangyu/sample.dotblog · GitHub

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo