Az alkalmazásunk fejlesztési szempontból késznek tekinthetÅ‘, de mivel a szoftverünket egyfajta szolgáltatásként (SAAS – Software as a service) szeretnénk továbbértékesÃteni, ezért gondolnunk kell az üzemeltetési oldalra.
Az üzemeltetésnél elsÅ‘ körben gondolnunk kell arra már a fejlesztés közben, hogy az API mögé kerülÅ‘ gép erÅ‘forrásai nem végtelenek. Éppen ezért a meglévÅ‘ erÅ‘forrással jól kell gazdálkodnunk. Ezen jó gazdálkodás egy módja a rate limiting, ami egy olyan mechanizmus, amivel korlátozni lehet, hogy egy felhasználó vagy kliens milyen gyakran hÃvhatja meg az API végpontokat.
De miért is van erre szükségünk? ElsÅ‘sorban azért, mert valaki szándékosan (DoS támadás) vagy véletlenül egy bug miatt rengeteg kérést küldhet, amit az API védelem nélkül nem fog bÃrni és a szolgáltatás le fog halni. De az is elÅ‘fordulhat, hogy a felhasználók közül nem szeretnénk, ha egyetlen egy lefoglalná az összes kapacitást. A rate limiting a költségkontroll szempontjából sem utolsó megoldás. A felhÅ‘szolgáltatások elÅ‘nye, hogy végtelenségig skálázódni tudnak, csak aztán gyÅ‘zzük kifizetni a számlát. Ha nem teszünk az API-ra egy korlátozást, akkor könnyen lehet, hogy a költségvetésünket többszörösen túllépjük.
De hogyan is valósÃtsuk meg ezt? Rate limiting esetén technológiától függetlenül aranyszabály, hogy ne kezdjük el kézzel mi magunk feltalálni újra a kereket, mert hamar rá fogunk jönni, hogy ez egy feneketlen kút probléma és a saját megoldásunk nem lesz annyira jó, mint egy már kész megoldás, aminek a fejlesztésébe hónapokat öltek. Szerencsére ASP.NET esetén van beépÃtett megoldást rate limitingre, több algoritmussal.
Algoritmusok
Az egyik legegyszerűbb algoritmus a fixed window. Ebben meghatározunk egy idÅ‘ablakot és egy megengedett számú kérést. Például azt mondjuk, hogy percenként 10 kérést engedünk kiszolgálni egy kliens irányába. Ha a kliens túllépte a limitet az adott percben, akkor egy HTTP 429 Too Many Requests hibával elutasÃtjuk azt. Ezen algoritmus elÅ‘nye, hogy egyszerű implementálni és gyorsan ellenÅ‘rizhetÅ‘. Hátránya, hogy torlódás tud kialakulni. Például a kliens az 59. másodpercben küld 100 kérést, ami a 60. másodpercben alaphelyzetbe áll, Ãgy a 61. másodpercben ismételten küldhet 100 kérést. Ez végeredményben azt jelenti, hogy 60 másodpercen belül akár 200 kérést is tud küldeni a kliens, ha jól idÅ‘zÃt.
Éppen ezért egy fejlettebb algoritmus a sliding window. Ez egy fix idÅ‘ablak helyett gördülÅ‘ módon nézi az idÅ‘t. Ezzel kiküszöbölhetÅ‘ a fixed window megoldás esetén ismertetett torlódás és sokkal stabilabb terheléselosztást biztosÃt. Hátránya viszont az, hogy ezt nehezebb implementálni, mert kell hozzá egy sor (queue) adatszerkezet és igen pontos idÅ‘számlálás.
A harmadik megoldás az úgynevezett token bucket algoritmus. Ez hasonló a sliding window megoldáshoz. Ennek a módszernek az alapja a vödör (bucket), amibe tockenek potyognak állandó, mondjuk 1 / 600ms sebességgel. A vödörnek van egy maximális kapacitása, ami legyen mondjuk 100. Minden kérés kiszolgálásakor 1 tokent kiveszünk a vödörbÅ‘l. Ha ez sikeres volt, akkor kiszolgálható a kliens, ha nem, akkor 429-es hibával elutasÃtjuk. Ezen megoldás elÅ‘nye, hogy rugalmas, kisebb idÅ‘intervallumon belül engedi a torlódást, de hosszú távon mégis olyan fair, mint a sliding window, de egyszerűbb implementálni.
A negyedik megoldás, hogy nem idÅ‘ alapon gondolkodunk, hanem az egyszerre futó kérések számában. Ezen megoldás a concurrency limiting. Például ha a szerver ki tud szolgálni 150 egyidejű kérést, akkor 150-ben limitáljuk a kérések számát. Ha a 150. kérés kiszolgálása közben érkezik egy 151. kérés, akkor azt elutasÃtjuk. Ezen megoldás hátránya, hogy a kérések számát nem korlátozza idÅ‘ben, Ãgy egy gyors kliens viheti a teljes kapacitást, ami rossz felhasználói élményt eredményezhet. Cserébe viszont elÅ‘nye, hogy védi a szervert a túlterheléstÅ‘l, egyszerűen implementálható és jól illeszkedik a lassabb műveletekhez. A való életben a concurrency megoldást ki szokták egészÃteni egy idÅ‘ alapú korlátozással is.
ASP.NET
ASP.NET esetén az ismertetett algoritmusok mindegyike használható egy beépÃtett middleware megoldáson keresztül. A használat elsÅ‘ lépéseként konfigurálnunk kell a limiting beállÃtásokat, majd a szolgáltatások közé az AddRateLimiter hÃvással fel kell vennünk a limitert.
var tokenPolicy = "global";
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = 100; //100 token lehet maximum
options.TokensPerPeriod = 100; //60 masodpercenkent 100 token
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; //legrégebbi kérések kerülnek először feldolgozásra
options.QueueLimit = 10; //maximum 10 kérés várakozhat
options.ReplenishmentPeriod = TimeSpan.FromSeconds(60); //60 másodpercenként újratöltődik a tokenek száma
options.AutoReplenishment = true; //automatikusan újratöltődik a tokenek száma
}));
Ezt követÅ‘en a builder.Build(); hÃvás után még használatba kell vennünk a rate limiting szolgáltatást az app.UseRateLimiter(); meghÃvásával.
Ezt követÅ‘en a RequireRateLimiting() hÃvással hozzá kell rendelnünk egy konfigurált szabályrendszert a végponthoz:
app.MapPost("/v1/urls", CreateUrlHandler.CreateUrl)
.RequireAuthorization()
.RequireRateLimiting(tokenPolicy);
app.MapGet("/v1/urls", GetUrlHandler.GetUrls)
.RequireAuthorization()
.RequireRateLimiting(tokenPolicy);
app.MapPut("/v1/{shortcode}", UpdateUrlHandler.UpdateUrl)
.RequireAuthorization()
.RequireRateLimiting(tokenPolicy);
app.MapDelete("/v1/{shortcode}", DeleteUrlHandler.DeleteUrl)
.RequireAuthorization()
.RequireRateLimiting(tokenPolicy);
app.MapGet("/v1/{shortcode}", RedirectUrlHandler.RedirectUrl)
.RequireRateLimiting(tokenPolicy);