Bagikan melalui


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 memperoleh RateLimitLease dari AcquireAsync.
  • lease.IsAcquired Ketika properti adalah true, permintaan dikirim ke server.
  • Jika tidak, HttpResponseMessage dikembalikan dengan 429 kode status, dan jika lease berisi RetryAfter nilai, header diatur ke nilai tersebut Retry-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 batas 8token , dan urutan pemrosesan antrean dari OldestFirst, batas antrean 3, dan periode 1 pengisian milidetik, token per nilai 2periode , dan nilai pengisian trueotomatis .
  • HttpClient Dibuat dengan yang dikonfigurasi ClientSideRateLimitedHandler dengan TokenBucketRateLimiter.
  • 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 mengirim GET 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.

Lihat juga