Batas laju handler HTTP di .NET
Dalam artikel ini, Anda akan mempelajari cara membuat handler HTTP sisi klien yang lajunya membatasi jumlah permintaan yang dikirimnya. Anda akan melihat HttpClient yang mengakses "www.example.com"
sumber daya. Sumber daya digunakan oleh aplikasi yang mengandalkannya, dan ketika aplikasi membuat terlalu banyak permintaan untuk satu sumber daya, itu dapat menyebabkan ketidakcocokan sumber daya. Pertikaian sumber daya terjadi ketika sumber daya dikonsumsi oleh terlalu banyak aplikasi, dan sumber daya tidak dapat melayani semua aplikasi yang memintanya. Ini dapat mengakibatkan pengalaman pengguna yang buruk, dan dalam beberapa kasus, bahkan dapat menyebabkan serangan penolakan layanan (DoS). Untuk informasi selengkapnya tentang DoS, lihat OWASP: Penolakan Layanan.
Apa itu pembatasan tarif?
Pembatasan tarif adalah konsep membatasi berapa banyak sumber daya yang dapat diakses. Misalnya, Anda mungkin tahu bahwa database yang diakses aplikasi Anda dapat menangani 1.000 permintaan per menit dengan aman, tetapi mungkin tidak menangani lebih dari itu. Anda dapat menempatkan pembatas tarif di aplikasi yang hanya mengizinkan 1.000 permintaan setiap menit dan menolak permintaan lagi sebelum mereka dapat mengakses database. Dengan demikian, tarif membatasi database Anda dan memungkinkan aplikasi Anda untuk menangani sejumlah permintaan yang aman. Ini adalah pola umum dalam sistem terdistribusi, di mana Anda mungkin memiliki beberapa instans aplikasi yang berjalan, dan Anda ingin memastikan bahwa mereka tidak semua mencoba mengakses database secara bersamaan. Ada beberapa algoritma pembatasan laju yang berbeda untuk mengontrol alur permintaan.
Untuk menggunakan pembatasan tarif di .NET, Anda akan mereferensikan paket System.Threading.RateLimiting NuGet.
Menerapkan subkelas DelegatingHandler
Untuk mengontrol alur permintaan, Anda menerapkan subkelas kustom DelegatingHandler . Ini adalah jenis HttpMessageHandler yang memungkinkan Anda untuk mencegat dan menangani permintaan sebelum dikirim ke server. Anda juga dapat mencegat dan menangani respons sebelum dikembalikan ke pemanggil. Dalam contoh ini, Anda akan menerapkan subkelas kustom DelegatingHandler
yang membatasi jumlah permintaan yang dapat dikirim ke satu sumber daya. Pertimbangkan kelas kustom ClientSideRateLimitedHandler
berikut:
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();
}
}
}
Kode C# sebelumnya:
- Mewarisi jenis.
DelegatingHandler
- IAsyncDisposable Mengimplementasikan antarmuka.
RateLimiter
Menentukan bidang yang ditetapkan dari konstruktor.- Mengambil alih
SendAsync
metode untuk mencegat dan menangani permintaan sebelum dikirim ke server. - Mengambil alih DisposeAsync() metode untuk membuang instans
RateLimiter
.
Melihat sedikit lebih dekat pada SendAsync
metode , Anda akan melihat bahwa itu:
- Bergantung pada
RateLimiter
instans untuk memperolehRateLimitLease
dariAcquireAsync
. lease.IsAcquired
Ketika properti adalahtrue
, permintaan dikirim ke server.- Jika tidak, HttpResponseMessage dikembalikan dengan
429
kode status, dan jikalease
berisiRetryAfter
nilai, header diatur ke nilai tersebutRetry-After
.
Meniru banyak permintaan bersamaan
Untuk menguji subkelas kustom DelegatingHandler
ini, Anda akan membuat aplikasi konsol yang meniru banyak permintaan bersamaan. Kelas ini Program
membuat HttpClient dengan kustom ClientSideRateLimitedHandler
:
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})");
}
Di aplikasi konsol sebelumnya:
- dikonfigurasi
TokenBucketRateLimiterOptions
dengan batas8
token , dan urutan pemrosesan antrean dariOldestFirst
, batas antrean3
, dan periode1
pengisian milidetik, token per nilai2
periode , dan nilai pengisiantrue
otomatis . HttpClient
Dibuat dengan yang dikonfigurasiClientSideRateLimitedHandler
denganTokenBucketRateLimiter
.- Untuk meniru 100 permintaan, Enumerable.Range buat 100 URL, masing-masing dengan parameter string kueri unik.
- Dua Task objek ditetapkan dari Parallel.ForEachAsync metode , membagi URL menjadi dua grup.
HttpClient
digunakan untuk mengirimGET
permintaan ke setiap URL, dan respons ditulis ke konsol.- Task.WhenAll menunggu kedua tugas selesai.
Karena dikonfigurasi HttpClient
dengan ClientSideRateLimitedHandler
, tidak semua permintaan akan membuatnya ke sumber daya server. Anda dapat menguji pernyataan ini dengan menjalankan aplikasi konsol. Anda akan melihat bahwa hanya sebagian kecil dari jumlah total permintaan yang dikirim ke server, dan sisanya ditolak dengan kode status HTTP .429
Coba ubah objek yang options
digunakan untuk membuat TokenBucketRateLimiter
untuk melihat bagaimana jumlah permintaan yang dikirim ke server berubah.
Pertimbangkan contoh output berikut:
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)
Anda akan melihat bahwa entri pertama yang dicatat selalu merupakan respons 429 yang segera dikembalikan, dan entri terakhir selalu merupakan 200 respons. Ini karena batas tarif ditemui sisi klien dan menghindari melakukan panggilan HTTP ke server. Ini adalah hal yang baik karena itu berarti bahwa server tidak dibanjiri permintaan. Ini juga berarti bahwa batas tarif diberlakukan secara konsisten di semua klien.
Perhatikan juga bahwa setiap string kueri URL bersifat unik: periksa iteration
parameter untuk melihat bahwa string kueri ditahapkan oleh satu untuk setiap permintaan. Parameter ini membantu menggambarkan bahwa 429 respons bukan dari permintaan pertama, melainkan dari permintaan yang dibuat setelah batas tarif tercapai. 200 respons tiba kemudian tetapi permintaan ini dibuat sebelumnya—sebelum batas tercapai.
Untuk memiliki pemahaman yang lebih baik tentang berbagai algoritma pembatasan laju, coba tulis ulang kode ini untuk menerima implementasi yang berbeda RateLimiter
. Selain yang TokenBucketRateLimiter
dapat Anda coba:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
Ringkasan
Dalam artikel ini, Anda mempelajari cara menerapkan kustom ClientSideRateLimitedHandler
. Pola ini dapat digunakan untuk menerapkan klien HTTP terbatas tarif untuk sumber daya yang Anda ketahui memiliki batas API. Dengan cara ini, Anda mencegah aplikasi klien membuat permintaan yang tidak perlu ke server, dan Anda juga mencegah aplikasi diblokir oleh server. Selain itu, dengan penggunaan metadata untuk menyimpan nilai waktu coba lagi, Anda juga dapat menerapkan logika coba lagi otomatis.