在 .NET 中限制 HTTP 處理常式的速率
在本文中,您將瞭解如何建立用戶端 HTTP 處理常式以限制其傳送的要求數目。 您會看到能存取資源 "www.example.com"
的 HttpClient。 需要這些資源的應用程式會取用資源,而當應用程式對單一資源提出太多要求時,則可能導致資源爭用。 當太多應用程式取用單一資源,而且資源不足以讓所有要求它的應用程式使用時,就會發生資源爭用。 這可能導致使用者體驗不佳,而在某些情況下,甚至可能導致拒絕服務 (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
執行個體從AcquireAsync
取得RateLimitLease
。 - 當
lease.IsAcquired
屬性為true
時,要求會傳送至伺服器。 - 否則,會傳回 HttpResponseMessage 和狀態碼
429
,而且如果lease
包含RetryAfter
值,Retry-After
標頭就會設定為該值。
模擬多個並行要求
若要測試此自訂 DelegatingHandler
子類別,您可以一個主控台應用程式來模擬多個並行要求。 Program
類別會建立具有自訂 ClientSideRateLimitedHandler
的 HttpClient:
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})");
}
在上述主控台應用程式中:
- 會以
8
的權杖限制、OldestFirst
的佇列處理順序、3
的佇列限制,以及補充時間1
毫秒、每單位時間權杖數2
個和自動補充值true
來設定TokenBucketRateLimiterOptions
。 HttpClient
是由TokenBucketRateLimiter
所設定的ClientSideRateLimitedHandler
所建立的。- 若要模擬 100 個要求,Enumerable.Range 會建立 100 個 URL,其中每個 URL 都有獨一無二的查詢字串參數。
- Parallel.ForEachAsync 方法會指派兩個 Task 物件,將 URL 分割成兩個群組。
HttpClient
可用來將GET
要求傳送至每個 URL,並將回應寫入主控台。- Task.WhenAll 會等候這兩項工作完成。
由於 HttpClient
是以 ClientSideRateLimitedHandler
設定的,所以並非所有要求都能抵達伺服器資源。 您可以執行主控台應用程式來測試此一主張。 您會發現全部的要求中只有一小部分能傳送到伺服器,其餘要求皆遭到拒絕 (HTTP 狀態碼為 429
)。 請嘗試改變用來建立 TokenBucketRateLimiter
的 options
物件來觀察傳送至伺服器的要求數量如何變化。
請考慮以下範例輸出:
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
之外,您也可以嘗試:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
摘要
在本文中,您已了解如何實作自訂 ClientSideRateLimitedHandler
。 此模式可用來針對已知有 API 限制的資源實作受速率限制的 HTTP 用戶端。 如此一來,您就能避免用戶端應用程式對伺服器提出不必要的要求,並且避免伺服器封鎖您的應用程式。 此外,您也可以利用中繼資料來儲存重試時間值以實作自動重試邏輯。