Share via


Ratenbegrenzung eines HTTP-Handlers in .NET

In diesem Artikel erfahren Sie, wie Sie einen clientseitigen HTTP-Handler erstellen, der eine Ratenbegrenzung auf die Anzahl der gesendeten Anforderungen anwendet. Es wird ein HttpClient angezeigt, der auf die "www.example.com"-Ressource zugreift. Ressourcen werden von Apps genutzt, die auf ihnen basieren, und wenn eine App zu viele Anforderungen an eine einzelne Ressource stellt, kann dies zu Ressourcenkonflikten führen. Ressourcenkonflikte treten auf, wenn eine Ressource von zu vielen Apps genutzt wird, und die Ressource nicht alle Apps bedienen kann, die sie anfordern. Daraus kann eine schlechte Benutzerfreundlichkeit resultieren, und in einigen Fällen kann es sogar zu einem Denial-of-Service-Angriff (DoS) führen. Weitere Informationen zu DoS finden Sie unter OWASP: Denial of Service.

Was bedeutet Ratenbegrenzung?

Die Ratenbegrenzung ist das Konzept der Beschränkung des Zugriffs auf eine Ressource. Beispielsweise wissen Sie vielleicht, dass eine Datenbank, auf die Ihre App zugreift, 1.000 Anforderungen pro Minute sicher verarbeiten kann, aber möglicherweise nicht viel mehr. Sie können eine Ratenbegrenzung in Ihre App einfügen, die nur 1.000 Anforderungen pro Minute zulässt und alle weiteren Anforderungen ablehnt, bevor sie auf die Datenbank zugreifen können. Legen Sie also eine Ratenbegrenzung für Ihre Datenbank fest, und lassen Sie eine sichere Anzahl von Anforderungen durch Ihre App zu. Dieses Muster ist in verteilten Systemen üblich, in denen möglicherweise mehrere Instanzen einer App ausgeführt werden, und Sie sicherstellen möchten, dass nicht alle gleichzeitig versuchen, auf die Datenbank zuzugreifen. Es gibt mehrere verschiedene Ratenbegrenzungsalgorithmen, um den Fluss von Anforderungen zu steuern.

Um die Ratenbegrenzung in .NET zu verwenden, nutzen Sie das System.Threading.RateLimiting-NuGet-Paket.

Implementieren einer DelegatingHandler-Unterklasse

Um den Fluss von Anforderungen zu steuern, implementieren Sie eine benutzerdefinierte DelegatingHandler-Unterklasse. Dies ist ein HttpMessageHandler-Typ, mit dem Sie Anforderungen abfangen und verarbeiten können, bevor sie an den Server gesendet werden. Sie können auch Antworten abfangen und behandeln, bevor sie an den Aufrufer zurückgegeben werden. In diesem Beispiel implementieren Sie eine benutzerdefinierte DelegatingHandler-Unterklasse, die die Anzahl der Anforderungen begrenzt, die an eine einzelne Ressource gesendet werden können. Betrachten wir einmal die folgende benutzerdefinierte 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();
        }
    }
}

Für den C#-Code oben gilt:

  • Erbt den DelegatingHandler-Typ.
  • Implementiert die IAsyncDisposable-Schnittstelle.
  • Definiert ein RateLimiter-Feld, das vom Konstruktor zugewiesen wird.
  • Überschreibt die SendAsync-Methode, um Anforderungen abzufangen und zu verarbeiten, bevor sie an den Server gesendet werden.
  • Überschreibt die DisposeAsync()-Methode, um die RateLimiter-Instanz zu verwerfen.

Wenn Sie sich die SendAsync-Methode etwas genauer ansehen, wird Ihnen Folgendes auffallen:

  • Basiert auf der RateLimiter-Instanz, um RateLimitLease aus AcquireAsync zu erhalten.
  • Wenn die lease.IsAcquired-Eigenschaft true ist, wird die Anforderung an den Server gesendet.
  • Andernfalls wird eine HttpResponseMessage mit einem 429-Statuscode zurückgegeben, und wenn lease einen RetryAfter-Wert enthält, wird der Retry-After-Header auf diesen Wert festgelegt.

Emulieren vieler gleichzeitiger Anforderungen

Um diese benutzerdefinierte DelegatingHandler-Unterklasse zu testen, erstellen Sie eine Konsolen-App, die viele gleichzeitige Anforderungen emuliert. Diese Program-Klasse erstellt einen HttpClient mit benutzerdefiniertem 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})");
}

In der obigen Konsolen-App:

  • Die TokenBucketRateLimiterOptions werden mit einem Tokenlimit von 8, der Reihenfolge der Warteschlangenverarbeitung von OldestFirst, einem Warteschlangenlimit von 3, einem Wiederauffüllungszeitraum von 1 Millisekunde, einem Token pro Periodenwert von 2 und einem Wert von true für automatisches Auffüllen konfiguriert.
  • Ein HttpClient wird mit dem ClientSideRateLimitedHandler erstellt, der mit dem TokenBucketRateLimiter konfiguriert ist.
  • Um 100 Anforderungen zu emulieren, erstellt Enumerable.Range 100 URLs mit jeweils einem eindeutigen Abfragezeichenfolgen-Parameter.
  • Zwei Task-Objekte werden von der Parallel.ForEachAsync-Methode zugewiesen, wobei die URLs in zwei Gruppen aufgeteilt werden.
  • Mit dem HttpClient wird eine GET-Anforderung an jede URL gesendet, und die Antwort wird in die Konsole geschrieben.
  • Task.WhenAll wartet, bis beide Aufgaben abgeschlossen sind.

Da HttpClient mit ClientSideRateLimitedHandler konfiguriert ist, werden nicht alle Anforderungen an die Serverressource gesendet. Sie können diese Assertion testen, indem Sie die Konsolen-App ausführen. Sie werden sehen, dass nur ein Bruchteil der Gesamtzahl der Anforderungen an den Server gesendet wird, und der Rest wird mit dem HTTP-Statuscode 429 abgelehnt. Versuchen Sie, das options-Objekt zu ändern, das zum Erstellen des TokenBucketRateLimiter verwendet wird, um zu sehen, wie sich die Anzahl der Anforderungen ändert, die an den Server gesendet werden.

Betrachten Sie die folgende Beispielausgabe:

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)

Sie werden feststellen, dass die ersten protokollierten Einträge immer die sofort zurückgegebenen 429 Antworten sind, und die letzten Einträge sind immer die 200 Antworten. Dies liegt daran, dass die Ratenbegrenzung clientseitig erfolgt und einen HTTP-Aufruf an einen Server vermeidet. Dies ist eine gute Sache, denn es bedeutet, dass der Server nicht mit Anforderungen überflutet wird. Es bedeutet auch, dass die Ratenbegrenzung konsistent für alle Clients erzwungen wird.

Beachten Sie auch, dass die Abfragezeichenfolge jeder URL eindeutig ist: Überprüfen Sie den iteration-Parameter, um festzustellen, ob er für jede Anforderung um eins erhöht wird. Dieser Parameter veranschaulicht, dass die 429 Antworten nicht von den ersten Anforderungen stammen, sondern von den Anforderungen, die nach Erreichen der Ratenbegrenzung gestellt werden. Die 200 Antworten treffen später ein, aber diese Anforderungen wurden früher gestellt – bevor der Grenzwert erreicht wurde.

Um die verschiedenen Ratenbegrenzungsalgorithmen besser zu verstehen, versuchen Sie, diesen Code neu zu schreiben, um eine andere RateLimiter-Implementierung zu akzeptieren. Zusätzlich zum TokenBucketRateLimiter können Sie Folgendes ausprobieren:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Zusammenfassung

In diesem Artikel haben Sie erfahren, wie Sie einen benutzerdefinierten ClientSideRateLimitedHandler implementieren. Dieses Muster kann verwendet werden, um einen HTTP-Client mit Ratenbegrenzung für Ressourcen zu implementieren, von denen Sie wissen, dass dafür API-Grenzwerte gelten. Auf diese Weise verhindern Sie, dass Ihre Client-App unnötige Anforderungen an den Server stellt, und Sie verhindern auch, dass Ihre App vom Server blockiert wird. Darüber hinaus können Sie durch Verwendung von Metadaten zum Speichern von Wiederholungszeitwerten auch automatische Wiederholungslogik implementieren.

Weitere Informationen