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 umRateLimitLease
doAcquireAsync
. - 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 olease
contiver um valorRetryAfter
, o cabeçalhoRetry-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 de8
, e a ordem de processamento de filaOldestFirst
, um limite de fila3
, um período de reabastecimento de1
milissegundos, um valor de tokens por período de2
e um valor de reabastecimento automático detrue
. - Um
HttpClient
é criado com oClientSideRateLimitedHandler
configurado com oTokenBucketRateLimiter
. - 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çãoGET
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.