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 "www.example.com" recurso. 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 à 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, classifique limitando seu banco de dados e permitindo que 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 NuGet System.Threading.RateLimiting .

Implementar uma DelegatingHandler subclasse

Para controlar o fluxo de solicitações, implemente uma subclasse personalizada DelegatingHandler . 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 personalizada DelegatingHandler que limita o número de solicitações que podem ser enviadas a um único recurso. Considere a seguinte classe personalizada 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();
        }
    }
}

O código C# anterior:

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

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

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

Emular muitas solicitações simultâneas

Para testar essa subclasse personalizada DelegatingHandler , você criará um aplicativo de console que emula muitas solicitações simultâneas. Esta Program classe cria um HttpClient com o personalizado 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})");
}

No aplicativo de console anterior:

  • Os TokenBucketRateLimiterOptions são configurados com um limite de token de , e ordem de 8processamento de fila de OldestFirst, um limite de fila de 3, e 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 que está 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 Task objetos são atribuídos a partir do Parallel.ForEachAsync método, dividindo as URLs em dois grupos.
  • O HttpClient é usado para enviar uma GET solicitação 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 options objeto 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 iteration parâmetro 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 este código para aceitar uma implementação diferente RateLimiter . Além do TokenBucketRateLimiter que você pode tentar:

  • ConcurrencyLimiter
  • FixedWindowRateLimiter
  • PartitionedRateLimiter
  • SlidingWindowRateLimiter

Resumo

Neste artigo, você aprendeu como implementar um arquivo ClientSideRateLimitedHandler. 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.

Consulte também