Delen via


Frequentielimiet voor een HTTP-handler in .NET

In dit artikel leert u hoe u een HTTP-handler aan de clientzijde maakt die het aantal aanvragen beperkt dat wordt verzonden. U ziet een HttpClient resource die toegang heeft tot de "www.example.com" resource. Resources worden gebruikt door apps die erop vertrouwen en wanneer een app te veel aanvragen voor één resource doet, kan dit leiden tot conflicten tussen resources. Resourceconflicten treden op wanneer een resource wordt verbruikt door te veel apps en de resource niet alle apps kan leveren die deze aanvraagt. Dit kan leiden tot een slechte gebruikerservaring en in sommige gevallen kan dit zelfs leiden tot een Denial of Service-aanval (DoS). Zie OWASP: Denial of Service voor meer informatie over DoS.

Wat is snelheidsbeperking?

Snelheidsbeperking is het concept van het beperken van de hoeveelheid toegang tot een resource. U weet bijvoorbeeld dat een database waartoe uw app toegang heeft, veilig 1000 aanvragen per minuut kan verwerken, maar dit kan niet veel meer verwerken. U kunt een snelheidslimiet instellen in uw app die slechts 1000 aanvragen elke minuut toestaat en alle aanvragen weigert voordat ze toegang hebben tot de database. Frequentiebeperking van uw database en het toestaan van uw app voor het afhandelen van een veilig aantal aanvragen. Dit is een algemeen patroon in gedistribueerde systemen, waarbij mogelijk meerdere exemplaren van een app worden uitgevoerd en u ervoor wilt zorgen dat ze niet allemaal tegelijkertijd toegang proberen te krijgen tot de database. Er zijn meerdere verschillende algoritmen voor snelheidsbeperking om de stroom van aanvragen te beheren.

Als u snelheidsbeperking in .NET wilt gebruiken, verwijst u naar het NuGet-pakket System.Threading.RateLimiting .

DelegatingHandler Een subklasse implementeren

Als u de stroom van aanvragen wilt beheren, implementeert u een aangepaste DelegatingHandler subklasse. Dit is een type HttpMessageHandler waarmee u aanvragen kunt onderscheppen en afhandelen voordat ze naar de server worden verzonden. U kunt ook antwoorden onderscheppen en afhandelen voordat ze worden geretourneerd naar de beller. In dit voorbeeld implementeert u een aangepaste DelegatingHandler subklasse die het aantal aanvragen beperkt dat naar één resource kan worden verzonden. Houd rekening met de volgende aangepaste ClientSideRateLimitedHandler klasse:

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();
        }
    }
}

De voorgaande C#-code:

  • Neemt het DelegatingHandler type over.
  • Implementeert de IAsyncDisposable interface.
  • Hiermee definieert u een RateLimiter veld dat is toegewezen vanuit de constructor.
  • Overschrijft de SendAsync methode voor het onderscheppen en verwerken van aanvragen voordat ze naar de server worden verzonden.
  • Hiermee wordt de DisposeAsync() methode voor het verwijderen van het RateLimiter exemplaar overschreven.

Als u een beetje dichter bij de SendAsync methode kijkt, ziet u dat:

  • Is afhankelijk van het RateLimiter exemplaar om een RateLimitLease van de AcquireAsync.
  • Wanneer de lease.IsAcquired eigenschap is true, wordt de aanvraag verzonden naar de server.
  • Anders wordt er een HttpResponseMessage geretourneerd met een 429 statuscode en als de lease waarde een RetryAfter waarde bevat, wordt de Retry-After header ingesteld op die waarde.

Veel gelijktijdige aanvragen emuleren

Als u deze aangepaste DelegatingHandler subklasse aan de test wilt toevoegen, maakt u een console-app die veel gelijktijdige aanvragen nabootst. Met deze Program klasse maakt u een HttpClient met de aangepaste ClientSideRateLimitedHandlerklasse:

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})");
}

In de voorgaande console-app:

  • Deze TokenBucketRateLimiterOptions zijn geconfigureerd met een tokenlimiet van 8, en wachtrijverwerkingsvolgorde van OldestFirst, een wachtrijlimiet van 3, en aanvullingsperiode van 1 milliseconden, een tokens per periode van 2, en een automatisch aanvullen waarde van true.
  • Er HttpClient wordt een gemaakt met de ClientSideRateLimitedHandler die is geconfigureerd met de TokenBucketRateLimiter.
  • Als u 100 aanvragen wilt emuleren, Enumerable.Range maakt u 100 URL's, elk met een unieke queryreeksparameter.
  • Er worden twee Task objecten uit de Parallel.ForEachAsync methode toegewezen, zodat de URL's in twee groepen worden gesplitst.
  • De HttpClient aanvraag wordt gebruikt om een GET aanvraag naar elke URL te verzenden en het antwoord wordt naar de console geschreven.
  • Task.WhenAll wacht tot beide taken zijn voltooid.

Omdat de HttpClient configuratie is geconfigureerd met de ClientSideRateLimitedHandler, niet alle aanvragen worden deze naar de serverresource verzonden. U kunt deze assertie testen door de console-app uit te voeren. U ziet dat slechts een fractie van het totale aantal aanvragen naar de server wordt verzonden en dat de rest wordt geweigerd met een HTTP-statuscode van 429. Wijzig het options object dat wordt gebruikt om het TokenBucketRateLimiter te maken om te zien hoe het aantal aanvragen dat naar de server wordt verzonden, verandert.

Bekijk de volgende voorbeelduitvoer:

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)

U ziet dat de eerste geregistreerde vermeldingen altijd de 429 antwoorden zijn die onmiddellijk worden geretourneerd en dat de laatste vermeldingen altijd de 200 antwoorden zijn. Dit komt doordat de frequentielimiet aan de clientzijde wordt aangetroffen en er geen HTTP-aanroep naar een server wordt gemaakt. Dit is handig omdat dit betekent dat de server niet wordt overspoeld met aanvragen. Dit betekent ook dat de frequentielimiet consistent wordt afgedwongen voor alle clients.

Houd er ook rekening mee dat de querytekenreeks van elke URL uniek is: bekijk de iteration parameter om te zien dat deze voor elke aanvraag wordt verhoogd. Deze parameter helpt u te illustreren dat de 429-antwoorden niet afkomstig zijn van de eerste aanvragen, maar van de aanvragen die worden gedaan nadat de frequentielimiet is bereikt. De 200 antwoorden komen later binnen, maar deze aanvragen zijn eerder gedaan, voordat de limiet werd bereikt.

Als u meer inzicht wilt krijgen in de verschillende frequentiebeperkingsalgoritmen, probeert u deze code te herschrijven om een andere RateLimiter implementatie te accepteren. Naast het TokenBucketRateLimiter volgende kunt u het volgende proberen:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Samenvatting

In dit artikel hebt u geleerd hoe u een aangepaste ClientSideRateLimitedHandlerimplementatie uitvoert. Dit patroon kan worden gebruikt voor het implementeren van een beperkte HTTP-client voor resources die u weet dat u API-limieten hebt. Op deze manier voorkomt u dat uw client-app onnodige aanvragen naar de server verzendt en u voorkomt dat uw app door de server wordt geblokkeerd. Daarnaast kunt u met het gebruik van metagegevens voor het opslaan van tijdsinstellingen voor opnieuw proberen ook automatische logica voor opnieuw proberen implementeren.

Zie ook