Compartilhar via


Limitar a taxa de um manipulador HTTP no .NET

Neste artigo, você aprenderá a criar um manipulador HTTP do lado do cliente que limita o número de solicitações enviadas. Você verá um HttpClient que acessa o recurso "www.example.com". Os recursos são consumidos por aplicativos que dependem deles e, quando um aplicativo faz muitas solicitações para um só recurso, ele pode levar à contenção de recursos. A contenção de recursos ocorre quando um recurso é consumido por muitos aplicativos e não consegue atender a todos os aplicativos que o estão solicitando. Isso pode resultar em uma experiência de usuário ruim e, em alguns casos, pode até mesmo levar a um ataque de DoS (negação de serviço). Para obter mais informações sobre a DoS, consulte OWASP: negação de serviço.

O que é a limitação de taxa?

Limitação de taxa é o conceito de limitar o quanto um recurso pode ser acessado. Por exemplo, talvez você saiba que um banco de dados que seu aplicativo acessa pode lidar com segurança com 1.000 solicitações por minuto, mas não pode lidar com muito mais do que isso. Você pode colocar um limitador de taxa no aplicativo que permite apenas 1.000 solicitações por minuto e rejeita mais solicitações antes que elas possam acessar o banco de dados. Assim, ele limita a taxa do banco de dados e permite que o aplicativo manipule um número seguro de solicitações. Esse é um padrão comum em sistemas distribuídos, em que você pode ter várias instâncias de um aplicativo em execução e deseja garantir que elas não tentem, todas, acessar o banco de dados ao mesmo tempo. Há vários algoritmos de limitação de taxa diferentes para controlar o fluxo de solicitações.

Para usar a limitação de taxa no .NET, você fará referência ao pacote NuGet System.Threading.RateLimiting.

Implementar uma subclasse DelegatingHandler

Para controlar o fluxo de solicitações, você implementa uma subclasse DelegatingHandler personalizada. Esse é um tipo de HttpMessageHandler que permite interceptar e manipular solicitações antes que elas sejam enviadas para o servidor. Você também pode interceptar e manipular respostas antes que elas sejam retornadas ao autor da chamada. Neste exemplo, você implementará uma subclasse DelegatingHandler personalizada que limita o número de solicitações que podem ser enviadas para um só recurso. Considere a seguinte classe ClientSideRateLimitedHandler personalizada:

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

O código anterior do C#:

  • Herda o tipo DelegatingHandler.
  • Implementa a interface IAsyncDisposable.
  • Define um campo RateLimiter atribuído do construtor.
  • Substitui o método SendAsync para interceptar e manipular solicitações antes de serem enviadas ao servidor.
  • Substitui o método DisposeAsync() para descartar a instância RateLimiter.

Olhando um pouco mais de perto para o método SendAsync, você verá que ele:

  • Depende da instância RateLimiter para adquirir um RateLimitLease do AcquireAsync.
  • Quando a propriedade lease.IsAcquired é true, a solicitação é enviada ao servidor.
  • Caso contrário, um HttpResponseMessage será retornado com um código de status 429 e, se o lease contiver um valor RetryAfter, o cabeçalho Retry-After será definido como esse valor.

Emular muitas solicitações simultâneas

Para testar essa subclasse DelegatingHandler personalizada, você criará um aplicativo de console que emula muitas solicitações simultâneas. Essa classe Program cria um HttpClient com o ClientSideRateLimitedHandler personalizado:

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

No aplicativo de console anterior:

  • Os TokenBucketRateLimiterOptions são configurados com um limite de token de 8, e a ordem de processamento de fila OldestFirst, um limite de fila 3, um período de reabastecimento de 1 milissegundos, um valor de tokens por período de 2 e um valor de reabastecimento automático de true.
  • Um HttpClient é criado com o ClientSideRateLimitedHandler configurado com o TokenBucketRateLimiter.
  • Para emular 100 solicitações, Enumerable.Range cria 100 URLs, cada uma com um parâmetro de cadeia de caracteres de consulta exclusivo.
  • Dois objetos Task são atribuídos do método Parallel.ForEachAsync, dividindo as URLs em dois grupos.
  • O HttpClient é usado para enviar uma solicitação GET para cada URL e a resposta é gravada no console.
  • Task.WhenAll aguarda a conclusão das duas tarefas.

Como HttpClient está configurado com ClientSideRateLimitedHandler, nem todas as solicitações chegarão ao recurso do servidor. Você pode testar essa declaração executando o aplicativo de console. Você verá que apenas uma fração do número total de solicitações é enviada ao servidor e o restante é rejeitado com um código de status HTTP 429. Tente alterar o objeto options usado para criar o TokenBucketRateLimiter para ver como o número de solicitações enviadas ao servidor é alterado.

Considere a seguinte saída de exemplo:

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)

Você observará que as primeiras entradas registradas são sempre as 429 respostas retornadas imediatamente, e as últimas entradas são sempre as 200 respostas. Isso ocorre porque o limite de taxa é encontrado no lado do cliente e evita fazer uma chamada HTTP para um servidor. Isso é bom porque significa que o servidor não está inundado de solicitações. Também significa que o limite de taxa é imposto consistentemente em todos os clientes.

Observe também que a cadeia de caracteres de consulta de cada URL é exclusiva: examine o parâmetro iteration para ver se ele é incrementado por um para cada solicitação. Esse parâmetro ajuda a ilustrar que as 429 respostas não são das primeiras solicitações, mas sim das solicitações feitas após o limite de taxa ser atingido. As 200 respostas chegam mais tarde, mas essas solicitações foram feitas anteriormente – antes do limite ser atingido.

Para ter uma melhor compreensão dos vários algoritmos de limitação de taxa, tente reescrever esse código para aceitar uma implementação de RateLimiter diferente. Além de TokenBucketRateLimiter. você pode tentar:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Resumo

Neste artigo, você aprendeu a implementar um ClientSideRateLimitedHandler personalizado. Esse padrão pode ser usado para implementar um cliente HTTP limitado por taxa para recursos que você sabe que têm limites de API. Dessa forma, você está impedindo que o aplicativo cliente faça solicitações desnecessárias para o servidor e também está impedindo que o aplicativo seja bloqueado pelo servidor. Além disso, com o uso de metadados para armazenar valores de tempo de repetição, você também pode implementar lógica de repetição automática.

Confira também