Partager via


Limite de débit d’un gestionnaire HTTP dans .NET

Dans cet article, vous allez apprendre à créer un gestionnaire HTTP côté client qui limite le nombre de requêtes qu’il envoie. Vous voyez un HttpClient qui accède à la ressource "www.example.com". Les ressources sont consommées par les applications qui s’appuient sur elles et lorsqu’une application effectue trop de requêtes pour une seule ressource, cela peut entraîner une contention de ressources. La contention de ressources se produit lorsqu’une ressource est consommée par un trop grand nombre d’applications et que la ressource ne peut pas servir toutes les applications qui la demandent. Cela peut entraîner une mauvaise expérience utilisateur et, dans certains cas, entraîner une attaque par déni de service (DoS). Pour plus d’informations sur DoS, consultez OWASP : Déni de service.

Qu’est-ce que la limitation du débit ?

La limitation du débit est le concept qui consiste à limiter la quantité d’accès à une ressource. Par exemple, vous savez peut-être qu’une base de données accessible par votre application peut gérer en toute sécurité 1 000 requêtes par minute, mais qu’elle ne peut pas gérer beaucoup plus que cela. Vous pouvez placer un limiteur de débit dans votre application qui n’autorise que 1 000 requêtes par minute et rejette toute requête supplémentaire avant qu’elle puisse accéder à la base de données. Cela permet donc de limiter le débit de votre base de données et permet à votre application de gérer un nombre sécurisé de requêtes. Il s’agit d’un modèle courant dans les systèmes distribués, où vous pouvez avoir plusieurs instances d’une application en cours d’exécution, et vous souhaitez vous assurer qu’elles n’essaient pas toutes d’accéder à la base de données en même temps. Il existe plusieurs algorithmes de limitation de débit différents pour contrôler le flux des requêtes.

Pour utiliser la limitation de débit dans .NET, vous allez référencer le package NuGet System.Threading.RateLimiting.

Implémenter une sous-classe DelegatingHandler

Pour contrôler le flux des requêtes, vous implémentez une sous-classe DelegatingHandler personnalisée. Il s’agit d’un type de HttpMessageHandler qui vous permet d’intercepter et de gérer les demandes avant qu’elles ne soient envoyées au serveur. Vous pouvez également intercepter et gérer les réponses avant qu’elles ne soient retournées à l’appelant. Dans cet exemple, vous allez implémenter une sous-classe DelegatingHandler personnalisée qui limite le nombre de requêtes pouvant être envoyées à une seule ressource. Considérez la classe ClientSideRateLimitedHandler personnalisée suivante :

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

Le code C# précédent :

  • Hérite du type DelegatingHandler.
  • Implémente l'interface IAsyncDisposable.
  • Définit un champ RateLimiter attribué à partir du constructeur.
  • Remplace la méthode SendAsync pour intercepter et gérer les requêtes avant qu’elles ne soient envoyées au serveur.
  • Remplace la méthode DisposeAsync() pour supprimer l’instance RateLimiter.

En regardant de plus près la méthode SendAsync, vous verrez qu’elle :

  • S’appuie sur l’instance RateLimiter pour acquérir un RateLimitLease à partir de AcquireAsync.
  • Lorsque la propriété lease.IsAcquired est true, la requête est envoyée au serveur.
  • Sinon, un HttpResponseMessage est retourné avec un code d’état 429 et, si lease contient une valeur RetryAfter, l’en-tête Retry-After est défini sur cette valeur.

Émuler de nombreuses requêtes simultanées

Pour tester cette sous-classe DelegatingHandler personnalisée, vous allez créer une application console qui émule de nombreuses requêtes simultanées. Cette classe Program crée un HttpClient avec le ClientSideRateLimitedHandler personnalisé :

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

Dans l’application console précédente :

  • Les TokenBucketRateLimiterOptions sont configurés avec une limite de jetons de 8 et l’ordre de traitement de la file d’attente de OldestFirst, une limite de file d’attente de 3 et une période de réapprovisionnement de 1 millisecondes, une valeur de jetons par période de 2 et une valeur de réapprovisionnement automatique de true.
  • Un HttpClient créé avec le ClientSideRateLimitedHandler qui est configuré avec le TokenBucketRateLimiter.
  • Pour émuler 100 requêtes, Enumerable.Range crée 100 URL, chacune avec un paramètre de chaîne de requête unique.
  • Deux objets Task sont affectés à partir de la méthode Parallel.ForEachAsync, fractionnant les URL en deux groupes.
  • HttpClient est utilisé pour envoyer une requête GET à chaque URL et la réponse est écrite dans la console.
  • Task.WhenAll attend que les deux tâches se terminent.

Étant donné que HttpClient est configuré avec le ClientSideRateLimitedHandler, toutes les requêtes ne sont pas envoyées à la ressource serveur. Vous pouvez tester cette assertion en exécutant l’application console. Vous verrez que seule une fraction du nombre total de requêtes est envoyée au serveur, et que les autres sont rejetées avec un code d’état HTTP de 429. Essayez de modifier l’objet options utilisé pour créer le TokenBucketRateLimiter pour voir comment le nombre de requêtes envoyées au serveur change.

Considérez l’exemple de sortie suivant :

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)

Vous remarquerez que les premières entrées journalisées sont toujours les 429 réponses immédiatement retournées, et les dernières entrées sont toujours les 200 réponses. En effet, la limite de débit est rencontrée côté client et évite d’effectuer un appel HTTP à un serveur. C’est une bonne chose, car cela signifie que le serveur n’est pas inondé de requêtes. Cela signifie également que la limite de débit est appliquée de manière cohérente à tous les clients.

Notez également que la chaîne de requête de chaque URL est unique : examinez le paramètre iteration pour voir qu’il est incrémenté de un pour chaque requête. Ce paramètre permet d’illustrer que les 429 réponses ne proviennent pas des premières requêtes, mais plutôt des requêtes effectuées une fois la limite de débit atteinte. Les 200 réponses arrivent plus tard, mais ces demandes ont été effectuées plus tôt, avant que la limite ne soit atteinte.

Pour mieux comprendre les différents algorithmes de limitation de débit, essayez de réécrire ce code pour accepter une implémentation RateLimiter différente. En plus de TokenBucketRateLimiter, vous pouvez essayer :

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Résumé

Dans cet article, vous avez appris à implémenter un ClientSideRateLimitedHandler personnalisé. Ce modèle peut être utilisé pour implémenter un client HTTP à débit limité pour les ressources dont vous savez qu’elles ont des limites d’API. De cette façon, vous empêchez votre application cliente d’effectuer des requêtes inutiles sur le serveur et vous empêchez également votre application d’être bloquée par le serveur. En outre, avec l’utilisation de métadonnées pour stocker les valeurs de minutage des nouvelles tentatives, vous pouvez également implémenter une logique de nouvelle tentative automatique.

Voir aussi