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 en RateLimitLease från AcquireAsync.
  • När egenskapen lease.IsAcquired är trueskickas begäran till servern.
  • Annars returneras en HttpResponseMessage med en 429 statuskod, och om det lease innehåller ett RetryAfter värde Retry-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äns 8på , och köbearbetningsordningen OldestFirst, en kögräns 3på , och påfyllningsperioden 1 på millisekunder, ett token per periodvärde på 2och ett automatiskt påfyllningsvärde på true.
  • En HttpClient skapas med ClientSideRateLimitedHandler som har konfigurerats med TokenBucketRateLimiter.
  • 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 en GET 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.

Se även