Partilhar via


Limite de taxa de um manipulador HTTP no .NET

Neste artigo, você aprenderá como criar um manipulador HTTP do lado do cliente que limita a taxa do 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 único recurso, isso pode levar a contenção de recursos. A contenção de recursos ocorre quando um recurso é consumido por muitos aplicativos e o recurso não consegue atender a todos os aplicativos que o solicitam. Isso pode resultar em uma experiência ruim do usuário e, em alguns casos, pode até levar a um ataque de negação de serviço (DoS). Para obter mais informações sobre DoS, consulte OWASP: Denial of Service.

O que é limitação de taxa?

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

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

Implementar uma subclasse DelegatingHandler

Para controlar o fluxo de solicitações, implemente uma subclasse DelegatingHandler personalizada. Este é um tipo de HttpMessageHandler que permite intercetar e lidar com solicitações antes que elas sejam enviadas para o servidor. Você também pode intercetar e manipular as respostas antes que elas sejam devolvidas ao chamador. Neste exemplo, você implementará uma subclasse DelegatingHandler personalizada que limita o número de solicitações que podem ser enviadas para um único recurso. Considere a seguinte classe de 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 C# anterior:

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

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

  • 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. Esta classe Program cria um HttpClient com o ClientSideRateLimitedHandlerpersonalizado:

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 fichas de 8, uma ordem de processamento da fila de OldestFirst, um limite de fila de 3e um período de reabastecimento de 1 milissegundos. O valor de fichas por período é de 2e o valor de reabastecimento automático é de true.
  • Um HttpClient é criado com o ClientSideRateLimitedHandler configurado com o TokenBucketRateLimiter.
  • Para emular 100 solicitações, o 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 a partir do método Parallel.ForEachAsync, dividindo as URLs em dois grupos.
  • O HttpClient é usado para enviar uma solicitação de GET para cada URL e a resposta é gravada no console.
  • Task.WhenAll aguarda a conclusão de ambas as tarefas.

Como o HttpClient está configurado com o ClientSideRateLimitedHandler, nem todas as solicitações chegarão ao recurso do servidor. Você pode testar essa afirmaçã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 de 429. Tente alterar o objeto options usado para criar o TokenBucketRateLimiter para ver como o número de solicitações enviadas ao servidor muda.

Considere o seguinte exemplo de saída:

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ê notará que as primeiras entradas registradas são sempre as 429 respostas imediatamente retornadas, 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. Isso também significa que o limite de taxa é aplicado de forma consistente 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 em 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 que são feitas depois que o limite de taxa é atingido. As 200 respostas chegam mais tarde, mas esses pedidos foram feitos antes, antes que o limite fosse 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 RateLimiter diferente. Além do TokenBucketRateLimiter você pode tentar:

Resumo

Neste artigo, você aprendeu como implementar um ClientSideRateLimitedHandlerpersonalizado. Esse padrão pode ser usado para implementar um cliente HTTP com taxa limitada para recursos que você sabe que têm limites de API. Dessa forma, você está impedindo que seu aplicativo cliente faça solicitações desnecessárias ao servidor e também está impedindo que seu 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 a lógica de repetição automática.

Ver também