Omezení rychlosti obslužné rutiny HTTP v .NET

V tomto článku se dozvíte, jak vytvořit obslužnou rutinu HTTP na straně klienta, která omezuje počet požadavků, které odesílá. Zobrazí se vám HttpClient přístup k "www.example.com" prostředku. Prostředky využívají aplikace, které na ně spoléhají, a když aplikace pro jeden prostředek vytváří příliš mnoho požadavků, může vést k kolizí prostředků. Kolize prostředků nastane, když prostředek spotřebuje příliš mnoho aplikací a prostředek nemůže obsluhovat všechny aplikace, které ho požadují. To může vést k špatnému uživatelskému prostředí a v některých případech může dokonce vést k útoku do odepření služby (DoS). Další informace o službě DoS najdete v tématu OWASP: Odepření služby.

Co je omezování rychlosti?

Omezování rychlosti je koncept omezení počtu prostředků, ke které je možné přistupovat. Můžete například vědět, že databáze, ke které vaše aplikace přistupuje, může bezpečně zpracovávat 1 000 požadavků za minutu, ale nemusí zpracovat mnohem více než to. Do aplikace můžete umístit omezovač rychlosti, který umožňuje 1 000 požadavků každou minutu a odmítne všechny další žádosti, než budou mít přístup k databázi. Proto omezte rychlost databáze a povolte aplikaci, aby zpracovávala bezpečný počet požadavků. Jedná se o běžný vzor v distribuovaných systémech, kde můžete mít spuštěných více instancí aplikace a chcete zajistit, aby se všechny nepokoušaly o přístup k databázi najednou. Existuje několik různých algoritmů omezování rychlosti pro řízení toku požadavků.

Pokud chcete použít omezení rychlosti v .NET, budete odkazovat na balíček NuGet System.Threading.RateLimiting .

DelegatingHandler Implementace podtřídy

Pokud chcete řídit tok požadavků, implementujete vlastní DelegatingHandler podtřídu. Jedná se o typ HttpMessageHandler , který umožňuje zachytávat a zpracovávat požadavky před jejich odesláním na server. Odpovědi můžete také zachytit a zpracovat předtím, než se vrátí volajícímu. V tomto příkladu implementujete vlastní DelegatingHandler podtřídu, která omezuje počet požadavků, které se dají odeslat do jednoho prostředku. Vezměte v úvahu následující vlastní ClientSideRateLimitedHandler třídu:

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

Předchozí kód jazyka C#:

  • Dědí DelegatingHandler typ.
  • Implementuje IAsyncDisposable rozhraní.
  • RateLimiter Definuje pole, které je přiřazeno z konstruktoru.
  • Přepíše metodu SendAsync pro zachycení a zpracování požadavků před jejich odesláním na server.
  • Přepíše metodu DisposeAsync() pro odstranění RateLimiter instance.

Při bližším pohledu na metodu SendAsync uvidíte, že:

  • Spoléhá na RateLimiter instanci k získání RateLimitLease z objektu AcquireAsync.
  • lease.IsAcquired Pokud je truevlastnost , požadavek se odešle na server.
  • V opačném případě je vrácena HttpResponseMessage se stavovým kódem 429 a pokud lease obsahuje RetryAfter hodnotu, hlavička Retry-After je nastavena na tuto hodnotu.

Emulace mnoha souběžných požadavků

Pokud chcete tuto vlastní DelegatingHandler podtřídu vložit do testu, vytvoříte konzolovou aplikaci, která emuluje mnoho souběžných požadavků. Tato Program třída vytvoří vlastníClientSideRateLimitedHandler:HttpClient

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

V předchozí aplikaci konzoly:

  • Konfigurují TokenBucketRateLimiterOptions se s limitem tokenu 8a pořadím zpracování fronty OldestFirst, limitem 3fronty a doplněním období 1 milisekund, tokeny na hodnotu 2období a automatické doplňování hodnoty true.
  • Vytvoří se HttpClient s ClientSideRateLimitedHandler nakonfigurovaným objektem TokenBucketRateLimiter.
  • Pro emulaci 100 požadavků Enumerable.Range vytvoří 100 adres URL, z nichž každý má jedinečný parametr řetězce dotazu.
  • Ze metody jsou přiřazeny Parallel.ForEachAsync dva Task objekty, které rozdělují adresy URL do dvou skupin.
  • Slouží HttpClient k odeslání GET požadavku na každou adresu URL a odpověď se zapíše do konzoly.
  • Task.WhenAll čeká na dokončení obou úloh.

HttpClient Vzhledem k tomu, že je nakonfigurovaná s nástrojem ClientSideRateLimitedHandler, ne všechny požadavky ho provedou do prostředku serveru. Tento kontrolní výraz můžete otestovat spuštěním konzolové aplikace. Uvidíte, že na server se odešle jenom zlomek celkového počtu požadavků a zbytek se odmítne se stavovým kódem 429HTTP . Zkuste změnit options objekt použitý k vytvoření TokenBucketRateLimiter , abyste viděli, jak se počet požadavků odesílaných na server mění.

Představte si následující příklad výstupu:

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)

Všimněte si, že první protokolované položky jsou vždy okamžitě vráceny 429 odpovědí a poslední položky jsou vždy 200 odpovědí. Důvodem je to, že je na straně klienta zjištěn limit rychlosti a zabraňuje volání PROTOKOLU HTTP na server. To je dobrá věc, protože to znamená, že server není zahlcený požadavky. Také to znamená, že se limit rychlosti vynucuje konzistentně napříč všemi klienty.

Všimněte si také, že řetězec dotazu každé adresy URL je jedinečný: prozkoumejte iteration parametr a zjistěte, že se pro každý požadavek zvýší o jeden. Tento parametr pomáhá ilustrovat, že odpovědi 429 nejsou z prvních požadavků, ale z požadavků provedených po dosažení limitu rychlosti. 200 odpovědí dorazí později, ale tyto žádosti byly provedeny dříve – před dosažením limitu.

Pokud chcete lépe porozumět různým algoritmům omezování rychlosti, zkuste tento kód přepsat a přijmout jinou RateLimiter implementaci. Kromě toho, co TokenBucketRateLimiter byste mohli vyzkoušet:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Shrnutí

V tomto článku jste se dozvěděli, jak implementovat vlastní ClientSideRateLimitedHandler. Tento model je možné použít k implementaci klienta HTTP s omezením rychlosti pro prostředky, které víte, že máte limity rozhraní API. Tímto způsobem bráníte klientské aplikaci v provádění nepotřebných požadavků na server a zároveň bráníte zablokování aplikace serverem. Kromě toho s použitím metadat k ukládání hodnot časování opakování můžete také implementovat logiku automatického opakování.

Viz také