Ограничение скорости обработчика HTTP в .NET

В этой статье вы узнаете, как создать клиентский обработчик HTTP, который ограничивает количество отправляемых запросов. Вы увидите HttpClient , что обращается к ресурсу "www.example.com" . Ресурсы используются приложениями, которые полагаются на них, и когда приложение делает слишком много запросов для одного ресурса, это может привести к возникновению спорных ресурсов. Состязание по ресурсам возникает, когда ресурс потребляется слишком большим количеством приложений, и ресурс не может обслуживать все приложения, запрашивающие его. Это может привести к плохому интерфейсу пользователя, и в некоторых случаях это может даже привести к атаке типа "отказ в обслуживании" (DoS). Дополнительные сведения о DoS см . в статье OWASP: отказ в обслуживании.

Что являет собой ограничения частоты?

Ограничение скорости — это концепция ограничения объема доступа к ресурсу. Например, вы можете знать, что доступ к базе данных приложения может безопасно обрабатывать 1000 запросов в минуту, но он может не обрабатывать гораздо больше, чем это. Вы можете поместить ограничение скорости в приложение, которое разрешает только 1000 запросов каждую минуту и отклоняет все больше запросов, прежде чем они смогут получить доступ к базе данных. Таким образом, ограничение скорости базы данных и разрешение приложению обрабатывать безопасное количество запросов. Это распространенный шаблон в распределенных системах, где может быть несколько экземпляров запущенного приложения, и вы хотите убедиться, что они не все пытаются получить доступ к базе данных одновременно. Существует несколько различных алгоритмов ограничения скорости для управления потоком запросов.

Чтобы использовать ограничение скорости в .NET, вы будете ссылаться на пакет NuGet System.Threading.RateLimiting .

Реализация подкласса DelegatingHandler

Для управления потоком запросов реализуется пользовательский DelegatingHandler подкласс. Это тип HttpMessageHandler , который позволяет перехватывать и обрабатывать запросы перед отправкой на сервер. Вы также можете перехватывать и обрабатывать ответы, прежде чем они будут возвращены вызывающей объекту. В этом примере вы реализуете пользовательский DelegatingHandler подкласс, ограничивающий количество запросов, которые могут отправляться одному ресурсу. Рассмотрим следующий пользовательский ClientSideRateLimitedHandler класс:

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

В приведенном выше коде C#:

  • Наследует DelegatingHandler тип.
  • Реализует интерфейс IAsyncDisposable.
  • Определяет RateLimiter поле, назначенное конструктором.
  • Переопределяет SendAsync метод для перехвата и обработки запросов перед отправкой на сервер.
  • Переопределяет DisposeAsync() метод для удаления экземпляра RateLimiter .

Посмотрите немного ближе к методу SendAsync , вы увидите, что это:

  • RateLimiter Используется экземпляр для получения RateLimitLease от объектаAcquireAsync.
  • lease.IsAcquired Когда свойство имеет значениеtrue, запрос отправляется серверу.
  • В противном случае возвращается HttpResponseMessage429 код состояния и lease если он содержит RetryAfter значение, Retry-After заголовок задается таким значением.

Эмулировать множество одновременных запросов

Чтобы поместить этот настраиваемый DelegatingHandler подкласс в тест, вы создадите консольное приложение, которое эмулирует множество одновременных запросов. Этот Program класс создает с HttpClient помощью пользовательского 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})");
}

В предыдущем консольном приложении:

  • Они TokenBucketRateLimiterOptions настраиваются с ограничением 8маркера и порядком OldestFirstобработки очередей, ограничением 3очереди и периодом 1 пополнения миллисекунда, маркерами за период 2и автоматическим заполнением.true
  • Создается HttpClient с ClientSideRateLimitedHandler настроенным параметром TokenBucketRateLimiter.
  • Чтобы эмулировать 100 запросов, Enumerable.Range создает 100 URL-адресов, каждый из которых имеет уникальный параметр строки запроса.
  • Два Task объекта назначаются из Parallel.ForEachAsync метода, разделяя URL-адреса на две группы.
  • Используется HttpClient для отправки GET запроса на каждый URL-адрес, а ответ записывается в консоль.
  • Task.WhenAll Ожидает завершения обеих задач.

Так как он HttpClient настроен с ClientSideRateLimitedHandlerпомощью не все запросы, они будут выполняться в ресурс сервера. Это утверждение можно проверить, выполнив консольное приложение. Вы увидите, что на сервер отправляется только часть общего количества запросов, а остальные отклоняются с кодом 429состояния HTTP. Попробуйте изменить options объект, используемый для создания TokenBucketRateLimiter , чтобы узнать, как количество запросов, отправляемых серверу.

Рассмотрим следующий пример выходных данных:

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)

Вы заметите, что первые записанные записи всегда являются немедленно возвращенными 429 ответами, а последние записи всегда являются ответами 200. Это связано с тем, что ограничение скорости встречается на стороне клиента и избегает вызова HTTP к серверу. Это хорошо, потому что это означает, что сервер не затоплен запросами. Это также означает, что ограничение скорости применяется согласованно для всех клиентов.

Обратите внимание, что строка запроса каждого URL-адреса уникальна: проверьте iteration параметр, чтобы увидеть, что он увеличивается по одному для каждого запроса. Этот параметр помогает иллюстрировать, что 429 ответов не из первых запросов, а не от запросов, сделанных после достижения ограничения скорости. 200 ответов поступают позже, но эти запросы были сделаны ранее, прежде чем было достигнуто ограничение.

Чтобы лучше понять различные алгоритмы ограничения скорости, попробуйте переписать этот код, чтобы принять другую RateLimiter реализацию. Помимо того, TokenBucketRateLimiter что вы можете попробовать:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Итоги

Из этой статьи вы узнали, как реализовать настраиваемый ClientSideRateLimitedHandler. Этот шаблон можно использовать для реализации ограниченного по скорости HTTP-клиента для ресурсов, которые вы знаете, имеют ограничения API. Таким образом, вы запрещаете клиентскому приложению делать ненужные запросы к серверу, и вы также не блокируете приложение сервером. Кроме того, при использовании метаданных для хранения значений времени повтора можно также реализовать логику автоматического повтора.

См. также