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 HttpClient che accede alla risorsa "www.example.com"
. 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. I confilitti di risorse si verificano quando una risorsa viene usata da troppe app e la risorsa non è in grado di gestire tutte le app che la richiedono. Ciò può comportare un'esperienza negativa per l'utente e, in alcuni casi, può addirittura causare un attacco Denial of Service (DoS). Per altre informazioni su DoS, vedere OWASP: Denial of Service.
Cos'è la limitazione della frequenza?
Il limite di frequenza è il concetto di limitazione della quantità di accesso a una risorsa. Ad esempio, si potrebbe essere a conoscenza del fatto che un database a cui l'app accede può gestire in modo sicuro 1.000 richieste al minuto, ma potrebbe non essere in grado di gestirne molte di più. È possibile inserire un limitatore di frequenza nell'app che consente solo 1.000 richieste ogni minuto e rifiuta qualsiasi altra richiesta prima di poter accedere al database. In questo modo, limita il database e permette all'applicazione 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 vari algoritmi di limite di frequenza diversi per controllare il flusso delle richieste.
Per usare il limite di frequenza in .NET, si farà riferimento al pacchetto NuGet System.Threading.RateLimiting .
Implementare una sottoclasse DelegatingHandler
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 seguente ClientSideRateLimitedHandler
:
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 campo
RateLimiter
assegnato dal costruttore. - Esegue l'override del metodo
SendAsync
per intercettare e gestire le richieste prima che vengano inviate al server. - Esegue l'override del metodo DisposeAsync() per eliminare l'istanza
RateLimiter
.
Osservando più attentamente il metodo SendAsync
, si noterà che:
- Si basa sull'istanza
RateLimiter
per acquisire unRateLimitLease
daAcquireAsync
. - Quando la proprietà
lease.IsAcquired
ètrue
, la richiesta viene inviata al server. - In caso contrario, viene restituito HttpResponseMessage con un codice di stato
429
e, selease
contiene un valoreRetryAfter
, l'intestazioneRetry-After
viene impostata su tale valore.
Emulare numerose richieste simultanee
Per inserire questa sottoclasse personalizzata DelegatingHandler
nel test, si creerà un'app console che emula varie richieste simultanee. Questa classe Program
crea un oggetto HttpClient con la personalizzazione 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:
TokenBucketRateLimiterOptions
vengono configurati con un limite di token di8
e l'ordine di elaborazione della coda diOldestFirst
, un limite di coda di3
e il periodo di rifornimento di1
millisecondi, un token per periodo di valore di2
e un valore di rifornimento automatico pari atrue
.- Viene creato
HttpClient
conClientSideRateLimitedHandler
configurato conTokenBucketRateLimiter
. - Per emulare 100 richieste, Enumerable.Range crea 100 URL, ognuno con un parametro di stringa di query univoco.
- Due oggetti Task vengono assegnati dal metodo Parallel.ForEachAsync, suddividendo gli URL in due gruppi.
HttpClient
viene usato per inviare una richiestaGET
a ogni URL e la risposta viene scritta nella console.- Task.WhenAll attende il completamento di entrambe le attività.
Poiché HttpClient
è configurato con ClientSideRateLimitedHandler
, non tutte le richieste verranno inviate alla risorsa server. È possibile testare questa asserzione eseguendo l'app console. Si noterà che solo una frazione del numero totale di richieste viene inviata al server, mentre il resto viene rifiutato con un codice di stato HTTP di 429
. Provare a modificare l'oggetto options
utilizzato per creare TokenBucketRateLimiter
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 429 risposte immediatamente restituite e le ultime voci sono sempre le 200 risposte. Ciò è dovuto al fatto che viene rilevato il limite di frequenza 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 frequenza viene applicato in modo coerente in tutti i client.
Si noti anche che la stringa di query di ogni URL è univoca: esaminare il parametro iteration
per vedere che viene incrementato di uno per ogni richiesta. Questo parametro consente di illustrare che le 429 risposte non provengono dalle prime richieste, ma piuttosto dalle richieste effettuate dopo il raggiungimento del limite di frequenza. 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 limite di frequenza, provare a riscrivere questo codice per accettare un'implementazione diversa RateLimiter
. Oltre a TokenBucketRateLimiter
, si potrebbe provare quanto segue:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
Riepilogo
In questo articolo, si è appreso come implementare una personalizzazione di ClientSideRateLimitedHandler
. Questo modello può essere usato per implementare un client HTTP con frequenza limitata per le risorse che è noto dispongano di limiti API. In questo modo, si impedisce all'app client di effettuare richieste non necessarie al server e si evita 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.