Hastighetsbegränsning för en HTTP-hanterare i .NET
I den här artikeln får du lära dig hur du skapar en HTTP-hanterare på klientsidan som begränsar antalet begäranden som skickas. Du ser en HttpClient som kommer åt resursen "www.example.com"
. Resurser förbrukas av appar som förlitar sig på dem, och när en app gör för många begäranden för en enskild resurs kan det leda till resurskonkurrering. Resurskonkurration uppstår när en resurs används av för många appar och resursen inte kan hantera alla appar som begär den. Detta kan leda till en dålig användarupplevelse, och i vissa fall kan det till och med leda till en DoS-attack (Denial of Service). Mer information om DoS finns i OWASP: Denial of Service.
Vad är hastighetsbegränsning?
Hastighetsbegränsning är konceptet att begränsa hur mycket en resurs kan nås. Du kanske till exempel vet att en databas som appen har åtkomst till på ett säkert sätt kan hantera 1 000 begäranden per minut, men den kanske inte hanterar mycket mer än så. Du kan placera en hastighetsbegränsning i din app som bara tillåter 1 000 begäranden varje minut och avvisar fler begäranden innan de kan komma åt databasen. Det innebär att du begränsar databasen och låter appen hantera ett säkert antal begäranden. Det här är ett vanligt mönster i distribuerade system, där du kan ha flera instanser av en app som körs, och du vill se till att alla inte försöker komma åt databasen samtidigt. Det finns flera olika frekvensbegränsningsalgoritmer för att styra flödet av begäranden.
Om du vill använda hastighetsbegränsning i .NET refererar du till NuGet-paketet System.Threading.RateLimiting .
Implementera en DelegatingHandler
underklass
Om du vill styra flödet av begäranden implementerar du en anpassad DelegatingHandler underklass. Det här är en typ av HttpMessageHandler som gör att du kan fånga upp och hantera begäranden innan de skickas till servern. Du kan också fånga upp och hantera svar innan de returneras till anroparen. I det här exemplet implementerar du en anpassad DelegatingHandler
underklass som begränsar antalet begäranden som kan skickas till en enskild resurs. Överväg följande anpassade ClientSideRateLimitedHandler
klass:
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();
}
}
}
Föregående C#-kod:
DelegatingHandler
Ärver typen.- Implementerar IAsyncDisposable gränssnittet.
- Definierar ett
RateLimiter
fält som har tilldelats från konstruktorn. - Åsidosätter
SendAsync
metoden för att fånga upp och hantera begäranden innan de skickas till servern. - Åsidosätter metoden DisposeAsync() för att ta bort instansen
RateLimiter
.
Om du tittar lite närmare på SendAsync
metoden ser du att den:
- Förlitar sig på instansen
RateLimiter
för att hämta enRateLimitLease
frånAcquireAsync
. - När egenskapen
lease.IsAcquired
ärtrue
skickas begäran till servern. - Annars returneras en HttpResponseMessage med en
429
statuskod, och om detlease
innehåller ettRetryAfter
värdeRetry-After
anges huvudet till det värdet.
Emulera många samtidiga begäranden
Om du vill testa den här anpassade DelegatingHandler
underklassen skapar du en konsolapp som emulerar många samtidiga begäranden. Den här Program
klassen skapar en HttpClient med anpassade 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})");
}
I föregående konsolapp:
TokenBucketRateLimiterOptions
Konfigureras med en tokengräns8
på , och köbearbetningsordningenOldestFirst
, en kögräns3
på , och påfyllningsperioden1
på millisekunder, ett token per periodvärde på2
och ett automatiskt påfyllningsvärde påtrue
.- En
HttpClient
skapas medClientSideRateLimitedHandler
som har konfigurerats medTokenBucketRateLimiter
. - Om du vill emulera 100 begäranden Enumerable.Range skapar du 100 URL:er, var och en med en unik frågesträngsparameter.
- Två Task objekt tilldelas från Parallel.ForEachAsync metoden och delar upp URL:erna i två grupper.
HttpClient
Används för att skicka enGET
begäran till varje URL och svaret skrivs till konsolen.- Task.WhenAll väntar på att båda aktiviteterna ska slutföras.
HttpClient
Eftersom är konfigurerad med ClientSideRateLimitedHandler
, kommer inte alla begäranden att göra det till serverresursen. Du kan testa den här försäkran genom att köra konsolappen. Du ser att endast en bråkdel av det totala antalet begäranden skickas till servern och resten avvisas med http-statuskoden 429
. Prova att ändra det options
objekt som används för att skapa TokenBucketRateLimiter
för att se hur antalet begäranden som skickas till servern ändras.
Överväg följande exempelutdata:
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)
Du kommer att märka att de första loggade posterna alltid är de omedelbart returnerade 429 svaren, och de sista posterna är alltid de 200 svaren. Det beror på att hastighetsgränsen påträffas på klientsidan och undviker att göra ett HTTP-anrop till en server. Detta är bra eftersom det innebär att servern inte översvämmas av begäranden. Det innebär också att hastighetsgränsen tillämpas konsekvent för alla klienter.
Observera också att varje URL:s frågesträng är unik: granska parametern iteration
för att se att den ökas med en för varje begäran. Den här parametern hjälper till att illustrera att 429-svaren inte kommer från de första begärandena, utan snarare från de begäranden som görs efter att hastighetsgränsen har nåtts. De 200 svaren kommer senare men dessa begäranden gjordes tidigare – innan gränsen uppnåddes.
Om du vill ha en bättre förståelse för de olika hastighetsbegränsningsalgoritmerna kan du prova att skriva om den här koden för att acceptera en annan RateLimiter
implementering. Utöver det TokenBucketRateLimiter
kan du prova:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
Sammanfattning
I den här artikeln har du lärt dig hur du implementerar en anpassad ClientSideRateLimitedHandler
. Det här mönstret kan användas för att implementera en hastighetsbegränsad HTTP-klient för resurser som du vet har API-gränser. På så sätt hindrar du klientappen från att göra onödiga begäranden till servern, och du förhindrar även att din app blockeras av servern. Med hjälp av metadata för att lagra tidsvärden för återförsök kan du dessutom implementera logik för automatiskt återförsök.