Udostępnij za pośrednictwem


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ć element RateLimitLease z klasy AcquireAsync.
  • Gdy lease.IsAcquired właściwość to true, żądanie jest wysyłane do serwera.
  • W przeciwnym razie zwracany HttpResponseMessage429 jest kod stanu, a jeśli lease element zawiera RetryAfter 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 ClientSideRateLimitedHandlerelementem :

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:

  • TokenBucketRateLimiterOptions one konfigurowane z limitem tokenów 8i kolejności OldestFirstprzetwarzania kolejek , limitem 3kolejki i okresem 1 uzupełniania milisekund, tokenami na wartość 2okresu i wartością trueautomatycznego uzupełniania wartości .
  • Element HttpClient jest tworzony przy użyciu elementu ClientSideRateLimitedHandler skonfigurowanego za pomocą polecenia TokenBucketRateLimiter.
  • 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łania GET żą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 ClientSideRateLimitedHandlerelementu , 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 ClientSideRateLimitedHandlerelementu . 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.

Zobacz też