.NET'te HTTP işleyicisi hız sınırı

Bu makalede, gönderdiği istek sayısını sınırlayan bir istemci tarafı HTTP işleyicisi oluşturmayı öğreneceksiniz. Kaynağa erişen "www.example.com" bir HttpClient görürsünüz. Kaynaklar, bu kaynaklara dayanan uygulamalar tarafından kullanılır ve bir uygulama tek bir kaynak için çok fazla istekte bulunursa kaynak çekişmesine yol açabilir. Kaynak çekişmesi, bir kaynak çok fazla uygulama tarafından kullanıldığında ve kaynak bunu isteyen tüm uygulamalara hizmet veremediğinde oluşur. Bu, kötü bir kullanıcı deneyimine neden olabilir ve bazı durumlarda hizmet reddi (DoS) saldırısına bile yol açabilir. DoS hakkında daha fazla bilgi için bkz . OWASP: Hizmet Reddi.

Hız sınırlama nedir?

Hız sınırlama, bir kaynağa ne kadar erişilebileceğini sınırlama kavramıdır. Örneğin, uygulamanızın eriştiği bir veritabanının dakikada 1.000 isteği güvenli bir şekilde işleyebileceğini biliyor olabilirsiniz, ancak bundan çok daha fazlasını işlemeyebilir. Uygulamanıza dakikada yalnızca 1.000 isteğe izin veren ve veritabanına erişebilmeleri için daha fazla isteği reddeden bir hız sınırlayıcısı koyabilirsiniz. Bu nedenle, veritabanınızı sınırlayan ve uygulamanızın güvenli sayıda isteği işlemesine izin veren hız. Bu, dağıtılmış sistemlerde yaygın olarak kullanılan bir desendir; burada bir uygulamanın birden çok örneği çalışıyor olabilir ve hepsinin aynı anda veritabanına erişmeye çalışmadığından emin olmak istersiniz. İstek akışını denetlemek için birden çok farklı hız sınırlama algoritması vardır.

.NET'te hız sınırlamayı kullanmak için System.Threading.RateLimiting NuGet paketine başvuracaksınız.

Alt sınıf uygulama DelegatingHandler

İstek akışını denetlemek için özel DelegatingHandler bir alt sınıf uygularsınız. Bu, istekleri sunucuya gönderilmeden önce kesmenize ve işlemenize olanak tanıyan bir türüdür HttpMessageHandler . Ayrıca, yanıtlar araya girip çağırana döndürülmeden önce de işleyebilirsiniz. Bu örnekte, tek bir kaynağa gönderilebilen istek sayısını sınırlayan özel DelegatingHandler bir alt sınıf uygulayacaksınız. Aşağıdaki özel ClientSideRateLimitedHandler sınıfı göz önünde bulundurun:

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

Yukarıdaki C# kodu:

  • Türü devralır DelegatingHandler .
  • Arabirimini IAsyncDisposable uygular.
  • Oluşturucudan atanan bir RateLimiter alanı tanımlar.
  • İstekleri SendAsync sunucuya gönderilmeden önce araya girip işlemek için yöntemini geçersiz kılar.
  • DisposeAsync() Örneği atmak RateLimiter için yöntemini geçersiz kılar.

yöntemine SendAsync biraz daha yakından baktığınızda şunu görürsünüz:

  • 'den bir RateLimitLease almak için örneğine AcquireAsyncdayanırRateLimiter.
  • lease.IsAcquired özelliği olduğunda trueistek sunucuya gönderilir.
  • Aksi takdirde, bir HttpResponseMessage429 durum kodu ile döndürülür ve lease değeri içeriyorsa RetryAfter , Retry-After üst bilgi bu değere ayarlanır.

Birçok eşzamanlı isteği öykünme

Bu özel DelegatingHandler alt sınıfı test etmek için birçok eşzamanlı isteği öykünen bir konsol uygulaması oluşturacaksınız. Bu Program sınıf, özel ClientSideRateLimitedHandlerile bir HttpClient oluşturur:

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

Önceki konsol uygulamasında:

  • TokenBucketRateLimiterOptions, belirteç sınırı 8ve kuyruk işleme sırasıOldestFirst, bir kuyruk sınırı 3ve milisaniyelik yenileme süresi1, dönem başına belirteç değeri 2ve değerinin otomatik yenileme değeri trueile yapılandırılır.
  • HttpClient ile ClientSideRateLimitedHandler yapılandırılan ile TokenBucketRateLimiteroluşturulur.
  • 100 isteği öykünmek için, Enumerable.Range her birinin benzersiz bir sorgu dizesi parametresine sahip olan 100 URL oluşturur.
  • yönteminden Parallel.ForEachAsync iki Task nesne atanır ve URL'ler iki gruba bölünür.
  • HttpClient her URL'ye istek GET göndermek için kullanılır ve yanıt konsola yazılır.
  • Task.WhenAll her iki görevi de tamamlamayı bekler.

HttpClient ile yapılandırıldığındanClientSideRateLimitedHandler, tüm istekler sunucu kaynağına yapamaz. Konsol uygulamasını çalıştırarak bu onaylama işlemini test edebilirsiniz. Sunucuya toplam istek sayısının yalnızca bir kısmının gönderildiğini ve geri kalanının HTTP durum koduyla 429reddedildiğini göreceksiniz. Sunucuya gönderilen isteklerin options sayısının nasıl değiştiğini görmek için öğesini oluşturmak TokenBucketRateLimiter için kullanılan nesnesini değiştirmeyi deneyin.

Aşağıdaki örnek çıkışı göz önünde bulundurun:

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)

İlk günlüğe kaydedilen girişlerin her zaman hemen döndürülen 429 yanıt olduğunu ve son girişlerin her zaman 200 yanıt olduğunu fark edeceksiniz. Bunun nedeni hız sınırıyla istemci tarafında karşılaşılması ve bir sunucuya HTTP çağrısı yapmaktan kaçınmasıdır. Sunucunun isteklerle dolu olmadığı anlamına geldiği için bu iyi bir şeydir. Bu, hız sınırının tüm istemciler arasında tutarlı bir şekilde zorlandığını da gösterir.

Her URL'nin sorgu dizesinin benzersiz olduğunu da unutmayın: Her istek için bir artırıldığını görmek için parametresini inceleyin iteration . Bu parametre, 429 yanıtlarının ilk isteklerden değil, hız sınırına ulaşıldıktan sonra yapılan isteklerden geldiğini göstermeye yardımcı olur. 200 yanıt daha sonra gelir, ancak bu istekler sınıra ulaşılamadan önce yapılmıştır.

Çeşitli hız sınırlama algoritmalarını daha iyi anlamak için farklı bir RateLimiter uygulamayı kabul etmek için bu kodu yeniden yazmayı deneyin. Aşağıdakilere TokenBucketRateLimiter ek olarak aşağıdakileri deneyebilirsiniz:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Özet

Bu makalede, özel ClientSideRateLimitedHandlerbir uygulamayı öğrendiniz. Bu desen, API sınırları olduğunu bildiğiniz kaynaklar için hız sınırlı bir HTTP istemcisi uygulamak için kullanılabilir. Bu şekilde, istemci uygulamanızın sunucuya gereksiz isteklerde bulunmasını engeller ve uygulamanızın sunucu tarafından engellenmesini de engellersiniz. Ayrıca, yeniden deneme zamanlaması değerlerini depolamak için meta verilerin kullanılmasıyla, otomatik yeniden deneme mantığını da uygulayabilirsiniz.

Ayrıca bkz.