Condividi tramite


Limite di frequenza di un gestore HTTP in .NET

In questo articolo si apprenderà come creare un gestore HTTP lato client che limita il numero di richieste inviate. Verrà visualizzato un oggetto HttpClient che accede alla "www.example.com" risorsa. Le risorse vengono utilizzate dalle app che si basano su di esse e quando un'app effettua troppe richieste per una singola risorsa, può causare conflitti di risorse. La contesa delle risorse si verifica quando una risorsa viene usata da troppe app e la risorsa non è in grado di gestire tutte le app che lo richiedono. Ciò può causare un'esperienza utente scarsa e, in alcuni casi, può anche causare un attacco Denial of Service (DoS). Per altre informazioni su DoS, vedere OWASP: Denial of Service.

Cos'è la limitazione della frequenza?

La limitazione della frequenza è il concetto di limitazione della quantità di accesso a una risorsa. Ad esempio, potresti sapere che un database a cui l'app accede può gestire in modo sicuro 1.000 richieste al minuto, ma potrebbe non gestire molto di più. È possibile inserire un limite di velocità nell'app che consente solo 1.000 richieste ogni minuto e rifiuta qualsiasi altra richiesta prima di poter accedere al database. Pertanto, applicare un limite al database e consentire all'app di gestire un numero sicuro di richieste. Si tratta di un modello comune nei sistemi distribuiti, in cui possono essere presenti più istanze di un'app in esecuzione e si vuole assicurarsi che non tutti tentino di accedere al database contemporaneamente. Esistono più algoritmi di limitazione della frequenza diversi per controllare il flusso delle richieste.

Per usare la limitazione della frequenza in .NET, si farà riferimento al pacchetto NuGet System.Threading.RateLimiting .

Implementare una DelegatingHandler sottoclasse

Per controllare il flusso delle richieste, implementare una sottoclasse personalizzata DelegatingHandler . Si tratta di un tipo di HttpMessageHandler che consente di intercettare e gestire le richieste prima che vengano inviate al server. È anche possibile intercettare e gestire le risposte prima che vengano restituite al chiamante. In questo esempio si implementerà una sottoclasse personalizzata DelegatingHandler che limita il numero di richieste che possono essere inviate a una singola risorsa. Si consideri la classe personalizzata ClientSideRateLimitedHandler seguente:

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

Il codice C# precedente:

  • Eredita il tipo DelegatingHandler.
  • Implementa l'interfaccia IAsyncDisposable.
  • Definisce un RateLimiter campo assegnato dal costruttore.
  • Esegue l'override del SendAsync metodo per intercettare e gestire le richieste prima che vengano inviate al server.
  • Esegue l'override del DisposeAsync() metodo per eliminare l'istanza RateLimiter .

Guardando un po' più vicino al SendAsync metodo, si noterà che:

  • Si basa sull'istanza RateLimiter per acquisire un RateLimitLease da AcquireAsync.
  • Quando la lease.IsAcquired proprietà è true, la richiesta viene inviata al server.
  • In caso contrario, viene restituito un oggetto HttpResponseMessage con un 429 codice di stato e, se lease contiene un RetryAfter valore, l'intestazione Retry-After viene impostata su tale valore.

Emulare molte richieste simultanee

Per inserire questa sottoclasse personalizzata DelegatingHandler al test, si creerà un'app console che emula molte richieste simultanee. Questa Program classe crea un oggetto HttpClient con l'oggetto personalizzato 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})");
}

Nell'app console precedente:

  • Sono configurati con un limite di token di TokenBucketRateLimiterOptions, un ordine di elaborazione della coda di 8, un limite di coda di OldestFirst, un periodo di rifornimento di 3 millisecondi, un valore di token per periodo di 1 e un valore di rifornimento automatico di 2.
  • Un HttpClient viene creato con l'oggetto ClientSideRateLimitedHandler configurato con il TokenBucketRateLimiter.
  • Per emulare 100 richieste, Enumerable.Range crea 100 URL, ognuno con un parametro di stringa di query univoco.
  • Due Task oggetti vengono assegnati dal Parallel.ForEachAsync metodo , suddividendo gli URL in due gruppi.
  • Il HttpClient viene usato per inviare una richiesta GET a ogni URL e la risposta viene inviata alla console.
  • Task.WhenAll attende il completamento di entrambe le attività.

Poiché il HttpClient è configurato con il ClientSideRateLimitedHandler, non tutte le richieste raggiungeranno la risorsa del server. È possibile testare questa asserzione eseguendo l'app console. Si noterà che solo una frazione del numero totale di richieste viene inviata al server e il resto viene rifiutato con un codice di stato HTTP di 429. Provare a modificare l'options oggetto utilizzato per creare il TokenBucketRateLimiter oggetto per vedere come cambia il numero di richieste inviate al server.

Si consideri l'output di esempio seguente:

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)

Si noterà che le prime voci registrate sono sempre le risposte di stato 429 immediatamente restituite, e le ultime voci sono sempre le risposte di stato 200. Ciò è dovuto al fatto che viene rilevato il limite di velocità sul lato client ed evita di effettuare una chiamata HTTP a un server. Questo è un aspetto positivo perché significa che il server non è inondato di richieste. Significa anche che il limite di velocità viene applicato in modo coerente in tutti i client.

Si noti anche che la stringa di query di ogni URL è univoca: esaminare il iteration parametro per vedere che viene incrementato di uno per ogni richiesta. Questo parametro consente di illustrare che le risposte 429 non provengono dalle prime richieste, ma piuttosto dalle richieste effettuate dopo il raggiungimento del limite di velocità. Le 200 risposte arrivano in un secondo momento, ma queste richieste sono state effettuate in precedenza, prima del raggiungimento del limite.

Per comprendere meglio i vari algoritmi di limitazione della frequenza, provare a riscrivere questo codice per accettare un'implementazione diversa RateLimiter . Oltre a TokenBucketRateLimiter si potrebbe provare:

Riassunto

In questo articolo si è appreso come implementare un oggetto personalizzato ClientSideRateLimitedHandler. Questo modello può essere usato per implementare un client HTTP con frequenza limitata per le risorse che si conoscono con limiti api. In questo modo, si impedisce all'app client di effettuare richieste non necessarie al server e si impedisce anche che l'app venga bloccata dal server. Inoltre, con l'uso dei metadati per archiviare i valori di intervallo dei tentativi, è anche possibile implementare la logica di ripetizione automatica dei tentativi.

Vedere anche