Megosztás a következőn keresztül:


HTTP-kezelő sebességkorlátozása a .NET-ben

Ebben a cikkben megtudhatja, hogyan hozhat létre ügyféloldali HTTP-kezelőt, amely korlátozza az általa küldött kérések számát. Megjelenik egy HttpClient, amely hozzáfér a "www.example.com" erőforráshoz. Az erőforrásokat azok az alkalmazások használják fel, amelyek támaszkodnak rájuk, és ha egy alkalmazás túl sok kérést küld egyetlen erőforrásra, az erőforrás-versengéshezvezethet. Az erőforrás-versengés akkor fordul elő, ha egy erőforrást túl sok alkalmazás használ fel, és az erőforrás nem tudja kiszolgálni az összes alkalmazást, amely kéri. Ez gyenge felhasználói élményt eredményezhet, és bizonyos esetekben akár szolgáltatásmegtagadási (DoS-) támadáshoz is vezethet. További információ a DoS-ról: OWASP: Szolgáltatásmegtagadási.

Mi az a sebességkorlátozás?

A sebességkorlátozás az erőforrás elérésének korlátozására vonatkozik. Előfordulhat például, hogy az alkalmazás által elért adatbázisok percenként 1000 kérést képesek biztonságosan kezelni, de ennél sokkal többet nem. Elhelyezhet egy sebességkorlátozót az alkalmazásban, amely percenként csak 1000 kérést engedélyez, és elutasítja a további kéréseket, mielőtt hozzáférnének az adatbázishoz. Így az adatbázis kérések sebességének korlátozása lehetővé teszi az alkalmazás számára, hogy biztonságos számú kérést kezeljen. Ez egy gyakori minta az elosztott rendszerekben, ahol előfordulhat, hogy egy alkalmazás több példánya is fut, és biztosítani szeretné, hogy ne egyszerre próbálják meg elérni az adatbázist. A kérések folyamatának szabályozásához több különböző sebességkorlátozó algoritmus is létezik.

Ha sebességkorlátozást szeretne használni a .NET-ben, hivatkoznia kell a System.Threading.RateLimiting NuGet-csomagra.

DelegatingHandler alosztály implementálása

A kérések folyamatának szabályozásához egyéni DelegatingHandler alosztályt kell implementálnia. Ez a HttpMessageHandler egy típusa, amely lehetővé teszi, hogy a kéréseket elfogja és kezelje, mielőtt a kiszolgálóra küldenék őket. A válaszok elfoghatók és kezelhetők, mielőtt visszakerülnének a hívóhoz. Ebben a példában egy egyéni DelegatingHandler alosztályt fog implementálni, amely korlátozza az egyetlen erőforrásnak küldhető kérelmek számát. Vegye figyelembe a következő egyéni ClientSideRateLimitedHandler osztályt:

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

Az előző C# kód:

  • Örökli a DelegatingHandler típust.
  • Implementálja a IAsyncDisposable felületet.
  • A konstruktortól hozzárendelt RateLimiter mezőt definiálja.
  • Felülbírálja a SendAsync metódust a kérések elfogására és kezelésére, mielőtt elküldené őket a kiszolgálónak.
  • Felülbírálja a DisposeAsync() metódust a RateLimiter példány megsemmisítéséhez.

Ha egy kicsit közelebbről tekinti meg a SendAsync metódust, a következőt fogja látni:

  • Egy RateLimitLease beszerzéséhez a RateLimiter példányra támaszkodik a AcquireAsync.
  • Ha a lease.IsAcquired tulajdonság true, a rendszer elküldi a kérést a kiszolgálónak.
  • Ellenkező esetben a HttpResponseMessage egy 429 állapotkóddal lesz visszaadva, és ha a leaseRetryAfter értéket tartalmaz, a Retry-After fejléc erre az értékre van beállítva.

Több egyidejű kérés emulálása

Ha ezt az egyéni DelegatingHandler alosztályt szeretné tesztelni, létre fog hozni egy konzolalkalmazást, amely számos egyidejű kérést emulál. Ez a Program osztály létrehoz egy HttpClient egyedi ClientSideRateLimitedHandler-t:

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

Az előző konzolalkalmazásban:

  • A TokenBucketRateLimiterOptions8jogkivonatkorláttal, a OldestFirstüzenetsor feldolgozási sorrendjével, a 3üzenetsorkorláttal és a 1 ezredmásodperc feltöltési időszakával, a 2időszakonkénti jogkivonatokkal és a trueautomatikus feltöltési értékével vannak konfigurálva .
  • Egy HttpClient jön létre a ClientSideRateLimitedHandler segítségével, amely a TokenBucketRateLimiter-vel van konfigurálva.
  • 100 kérés emulálásához Enumerable.Range 100 URL-címet hoz létre, amelyek mindegyike egyedi lekérdezési sztringparaméterrel rendelkezik.
  • Két Task objektum van hozzárendelve a Parallel.ForEachAsync metódusból, és az URL-címeket két csoportra osztja.
  • A HttpClient egy GET kérés elküldésére szolgál minden URL-címre, a válasz pedig a konzolra lesz írva.
  • Task.WhenAll megvárja, amíg mindkét tevékenység befejeződik.

Mivel a HttpClient a ClientSideRateLimitedHandlervan konfigurálva, nem minden kérés jut el a kiszolgáló erőforrásához. Ezt az állítást a konzolalkalmazás futtatásával tesztelheti. Látni fogja, hogy a rendszer csak a kérések teljes számának töredékét küldi el a kiszolgálónak, a többit pedig a rendszer elutasítja a 429HTTP-állapotkódjával. Próbálja meg módosítani a TokenBucketRateLimiter létrehozásához használt options objektumot, hogy lássa, hogyan változik a kiszolgálónak küldött kérések száma.

Vegye figyelembe a következő példakimenetet:

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)

Megfigyelheti, hogy az első naplózott bejegyzések mindig az azonnal visszaadott 429 válasz, az utolsó bejegyzések pedig mindig a 200 válasz. Ennek az az oka, hogy a sebességkorlát ügyféloldali, és elkerüli, hogy HTTP-hívást kezdeményezzenek a kiszolgálóhoz. Ez azért jó, mert azt jelenti, hogy a kiszolgálót nem elárasztják a kérések. Azt is jelenti, hogy a sebességkorlátot minden ügyfél következetesen érvényesíti.

Vegye figyelembe azt is, hogy az egyes URL-címek lekérdezési sztringje egyedi: vizsgálja meg a iteration paramétert, és ellenőrizze, hogy minden egyes kérésnél egyel növekszik-e. Ez a paraméter segít szemléltetni, hogy a 429 válasz nem az első kérésekből származik, hanem a sebességkorlát elérése után küldött kérelmekből. A 200 válasz később érkezik, de ezek a kérések korábban érkeztek – a korlát elérése előtt.

A különböző sebességkorlátozó algoritmusok jobb megértéséhez próbálja meg újraírni ezt a kódot egy másik RateLimiter implementáció elfogadásához. A TokenBucketRateLimiter mellett a következőt is kipróbálhatja:

Összefoglalás

Ebben a cikkben megtudta, hogyan lehet egyéni ClientSideRateLimitedHandler-t megvalósítani. Ez a minta használható egy korlátozott sebességű HTTP-ügyfél implementálásához olyan erőforrásokhoz, amelyekről tudja, hogy API-korlátokkal rendelkeznek. Ily módon megakadályozza, hogy az ügyfélalkalmazás szükségtelen kéréseket küldjön a kiszolgálónak, és azt is megakadályozza, hogy a kiszolgáló letiltsa az alkalmazást. Emellett az újrapróbálkozás időzítési értékeinek tárolására szolgáló metaadatok használatával automatikus újrapróbálkoztatási logikát is implementálhat.

Lásd még: