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 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 resourceconflicten. 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 Servicevoor 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 System.Threading.RateLimiting NuGet-pakket.
Een DelegatingHandler
-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. - Overschrijft de DisposeAsync()-methode om de
RateLimiter
-instantie op te ruimen.
Als u de SendAsync
methode nader bekijkt, ziet u dat:
- Het is afhankelijk van het
RateLimiter
exemplaar om eenRateLimitLease
te verkrijgen van deAcquireAsync
. - Wanneer de eigenschap
lease.IsAcquired
istrue
, wordt de aanvraag naar de server verzonden. - Anders wordt een HttpResponseMessage geretourneerd met een
429
statuscode en als delease
eenRetryAfter
waarde bevat, wordt deRetry-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 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 de voorgaande console-app:
- De
TokenBucketRateLimiterOptions
zijn geconfigureerd met een tokenlimiet van8
, een wachtrijverwerkingsvolgorde vanOldestFirst
, een wachtrijlimiet van3
, een aanvullingsperiode van1
milliseconden, een aantal tokens per periode van2
en een waarde voor automatisch aanvullen vantrue
. - Er wordt een
HttpClient
gemaakt met deClientSideRateLimitedHandler
die is geconfigureerd met deTokenBucketRateLimiter
. - Als u 100 aanvragen wilt emuleren, Enumerable.Range 100 URL's maakt, elk met een unieke querytekenreeksparameter.
- Er worden twee Task objecten toegewezen vanuit de methode Parallel.ForEachAsync, zodat de URL's in twee groepen worden gesplitst.
- De
HttpClient
wordt gebruikt om eenGET
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
is geconfigureerd met de ClientSideRateLimitedHandler
, worden niet alle aanvragen 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 de 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 snelheidslimiet consistent wordt afgedwongen voor alle klanten.
Houd er ook rekening mee dat de querytekenreeks van elke URL uniek is: bekijk de iteration
parameter om te zien dat deze wordt verhoogd met één voor elke aanvraag. 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 de TokenBucketRateLimiter kunt u het volgende proberen:
Samenvatting
In dit artikel hebt u geleerd hoe u een aangepaste ClientSideRateLimitedHandler
implementeert. Dit patroon kan worden gebruikt voor het implementeren van een HTTP-client met een snelheidslimiet voor resources waarvan u weet dat er API-limieten zijn. 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.