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 aRateLimiter
példányra támaszkodik aAcquireAsync
. - Ha a
lease.IsAcquired
tulajdonságtrue
, a rendszer elküldi a kérést a kiszolgálónak. - Ellenkező esetben a HttpResponseMessage egy
429
állapotkóddal lesz visszaadva, és ha alease
RetryAfter
értéket tartalmaz, aRetry-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
TokenBucketRateLimiterOptions
8
jogkivonatkorláttal, aOldestFirst
üzenetsor feldolgozási sorrendjével, a3
üzenetsorkorláttal és a1
ezredmásodperc feltöltési időszakával, a2
időszakonkénti jogkivonatokkal és atrue
automatikus feltöltési értékével vannak konfigurálva . - Egy
HttpClient
jön létre aClientSideRateLimitedHandler
segítségével, amely aTokenBucketRateLimiter
-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
egyGET
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 ClientSideRateLimitedHandler
van 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 429
HTTP-á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:
- .NET- sebességkorlátozásának bejelentése
- Köztes szoftver sebességkorlátozása ASP.NET Core
- Azure-architektúra: Sebességkorlátozó minta
- Automatikus újrapróbálkozás logikája a .NET-