Ograniczanie szybkości programu obsługi HTTP na platformie .NET
W tym artykule dowiesz się, jak utworzyć program obsługi HTTP po stronie klienta, który ogranicza liczbę wysyłanych żądań. Zobaczysz, HttpClient że uzyskuje dostęp do "www.example.com"
zasobu. Zasoby są używane przez aplikacje, które polegają na nich, a gdy aplikacja wysyła zbyt wiele żądań dla pojedynczego zasobu, może to prowadzić do rywalizacji o zasoby. Rywalizacja o zasoby występuje, gdy zasób jest używany przez zbyt wiele aplikacji, a zasób nie może obsłużyć wszystkich aplikacji, które żądają. Może to spowodować słabe środowisko użytkownika, a w niektórych przypadkach może to nawet prowadzić do ataku typu "odmowa usługi" (DoS). Aby uzyskać więcej informacji na temat usługi DoS, zobacz OWASP: Odmowa usługi.
Co to jest ograniczanie szybkości?
Ograniczanie szybkości to koncepcja ograniczania ilości zasobów, do których można uzyskać dostęp. Na przykład możesz wiedzieć, że baza danych, do której uzyskuje dostęp aplikacja, może bezpiecznie obsługiwać 1000 żądań na minutę, ale może nie obsłużyć znacznie więcej. W aplikacji można umieścić ograniczenie szybkości, które zezwala tylko na 1000 żądań co minutę i odrzuca więcej żądań, zanim będą mogły uzyskać dostęp do bazy danych. W związku z tym szybkość ograniczania bazy danych i zezwalania aplikacji na obsługę bezpiecznej liczby żądań. Jest to typowy wzorzec w systemach rozproszonych, w którym może istnieć wiele wystąpień aplikacji i chcesz upewnić się, że nie wszystkie próbują uzyskać dostęp do bazy danych w tym samym czasie. Istnieje wiele różnych algorytmów ograniczania szybkości w celu kontrolowania przepływu żądań.
Aby użyć ograniczania szybkości na platformie .NET, odwołasz się do pakietu NuGet System.Threading.RateLimiting .
Implementowanie podklasy DelegatingHandler
Aby kontrolować przepływ żądań, należy zaimplementować niestandardową DelegatingHandler podklasę. Jest to typ HttpMessageHandler , który umożliwia przechwytywanie i obsługę żądań przed ich wysłaniem do serwera. Możesz również przechwytywać i obsługiwać odpowiedzi, zanim zostaną zwrócone do elementu wywołującego. W tym przykładzie zaimplementujesz niestandardową DelegatingHandler
podklasę, która ogranicza liczbę żądań, które mogą być wysyłane do pojedynczego zasobu. Rozważmy następującą klasę niestandardową 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();
}
}
}
Poprzedni kod języka C#:
- Dziedziczy
DelegatingHandler
typ. - Implementuje IAsyncDisposable interfejs.
RateLimiter
Definiuje pole przypisane z konstruktora.- Zastępuje metodę przechwytywania
SendAsync
i obsługi żądań przed wysłaniem ich na serwer. - Zastępuje metodę DisposeAsync() usuwania
RateLimiter
wystąpienia.
Patrząc nieco bliżej SendAsync
metody, zobaczysz, że:
- Opiera się na wystąpieniu
RateLimiter
, aby uzyskać elementRateLimitLease
z klasyAcquireAsync
. - Gdy
lease.IsAcquired
właściwość totrue
, żądanie jest wysyłane do serwera. - W przeciwnym razie zwracany HttpResponseMessage
429
jest kod stanu, a jeślilease
element zawieraRetryAfter
wartość,Retry-After
nagłówek jest ustawiony na wartość .
Emuluj wiele współbieżnych żądań
Aby umieścić tę niestandardową DelegatingHandler
podklasę w teście, utworzysz aplikację konsolową, która emuluje wiele współbieżnych żądań. Ta Program
klasa tworzy obiekt HttpClient z niestandardowym ClientSideRateLimitedHandler
elementem :
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})");
}
W poprzedniej aplikacji konsolowej:
- Są
TokenBucketRateLimiterOptions
one konfigurowane z limitem tokenów8
i kolejnościOldestFirst
przetwarzania kolejek , limitem3
kolejki i okresem1
uzupełniania milisekund, tokenami na wartość2
okresu i wartościątrue
automatycznego uzupełniania wartości . - Element
HttpClient
jest tworzony przy użyciu elementuClientSideRateLimitedHandler
skonfigurowanego za pomocą poleceniaTokenBucketRateLimiter
. - Aby emulować 100 żądań, Enumerable.Range tworzy 100 adresów URL, z których każdy ma unikatowy parametr ciągu zapytania.
- Dwa Task obiekty są przypisywane z Parallel.ForEachAsync metody, dzieląc adresy URL na dwie grupy.
- Element
HttpClient
służy do wysyłaniaGET
żądania do każdego adresu URL, a odpowiedź jest zapisywana w konsoli programu . - Task.WhenAll czeka na ukończenie obu zadań.
Ponieważ parametr HttpClient
jest skonfigurowany przy użyciu ClientSideRateLimitedHandler
elementu , nie wszystkie żądania będą wysyłane do zasobu serwera. Tę asercję można przetestować, uruchamiając aplikację konsolową. Zobaczysz, że do serwera jest wysyłany tylko ułamek całkowitej liczby żądań, a reszta jest odrzucana przy użyciu kodu stanu HTTP .429
Spróbuj zmienić options
obiekt użyty do utworzenia TokenBucketRateLimiter
obiektu , aby zobaczyć, jak liczba żądań wysyłanych do serwera zmienia się.
Rozważmy następujące przykładowe dane wyjściowe:
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)
Zauważysz, że pierwsze zarejestrowane wpisy są zawsze natychmiast zwracane 429 odpowiedzi, a ostatnie wpisy są zawsze odpowiedziami 200. Dzieje się tak, ponieważ występuje limit szybkości po stronie klienta i unika wykonywania wywołania HTTP na serwerze. Jest to dobra rzecz, ponieważ oznacza to, że serwer nie jest zalany żądaniami. Oznacza to również, że limit szybkości jest wymuszany spójnie we wszystkich klientach.
Należy również pamiętać, że ciąg zapytania każdego adresu URL jest unikatowy: sprawdź iteration
parametr, aby zobaczyć, że jest zwiększany o jeden dla każdego żądania. Ten parametr pomaga zilustrować, że odpowiedzi 429 nie pochodzą z pierwszych żądań, ale raczej z żądań, które są wykonywane po osiągnięciu limitu szybkości. 200 odpowiedzi pojawi się później, ale te żądania zostały złożone wcześniej — przed osiągnięciem limitu.
Aby lepiej zrozumieć różne algorytmy ograniczania szybkości, spróbuj ponownie zapisać ten kod, aby zaakceptować inną RateLimiter
implementację. Oprócz TokenBucketRateLimiter
tego możesz spróbować wykonać następujące próby:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
Podsumowanie
W tym artykule przedstawiono sposób implementowania niestandardowego ClientSideRateLimitedHandler
elementu . Ten wzorzec może służyć do implementowania klienta HTTP z ograniczoną szybkością dla zasobów, o których wiesz, że istnieją limity interfejsu API. W ten sposób uniemożliwiasz aplikacji klienckiej wykonywanie niepotrzebnych żądań na serwerze i uniemożliwiasz zablokowanie aplikacji przez serwer. Ponadto przy użyciu metadanych do przechowywania wartości chronometrażu ponawiania prób można również zaimplementować automatyczną logikę ponawiania prób.