Límite de frecuencia de un controlador HTTP en .NET
En este artículo, aprenderá a crear un controlador HTTP del lado cliente que limite la frecuencia del número de solicitudes que envía. Verá un HttpClient que accede al recurso "www.example.com"
. Las aplicaciones consumen recursos de los que dependen y, cuando una aplicación realiza demasiadas solicitudes relativas un mismo recurso, puede provocar una contención de recursos. La contención de recursos se produce cuando hay demasiadas aplicaciones que consumen un recurso, y este no puede atender a todas las aplicaciones solicitantes. Esto puede dar lugar a una experiencia de usuario deficiente y, en algunos casos, incluso puede provocar un ataque por denegación de servicio (DoS). Para obtener más información sobre DoS, consulte OWASP: denegación de servicio.
¿Qué es la limitación de frecuencia?
La limitación de frecuencia consiste en limitar la cantidad de un recurso a la que se puede acceder. Por ejemplo, podemos saber que una base de datos a la que accede la aplicación puede controlar de forma segura unas 1000 solicitudes por minuto, pero es posible que no controle mucho más que eso. Se puede colocar un limitador de frecuencia en la aplicación que permita únicamente 1000 solicitudes cada minuto y que rechace el resto de solicitudes antes de que puedan acceder a la base de datos. Dicho de otro modo, limitaríamos la frecuencia de acceso a la base de datos y nos aseguraríamos de que la aplicación controla un número de solicitudes seguro. Se trata de algo bastante habitual en los sistemas distribuidos, donde puede haber varias instancias de una aplicación en ejecución y queremos asegurarnos de que no intentan acceder a la base de datos al mismo tiempo. Existen distintos algoritmos de limitación de frecuencia para controlar el flujo de solicitudes.
Para usar la limitación de frecuencia en .NET, haremos referencia al paquete NuGet System.Threading.RateLimiting.
Implementar una DelegatingHandler
subclase
Para controlar el flujo de solicitudes, hay que implementar una subclase DelegatingHandler personalizada. Se trata de un tipo de objeto HttpMessageHandler que permite interceptar y controlar las solicitudes antes de que se envíen al servidor. También puede interceptar y controlar las respuestas antes de que se devuelvan al autor de la llamada. En este ejemplo, se implementará una subclase DelegatingHandler
personalizada que limita el número de solicitudes que se pueden enviar a un mismo recurso. Fíjese en la siguiente clase 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();
}
}
}
El código de C# anterior:
- Hereda el tipo
DelegatingHandler
. - Implementa la interfaz IAsyncDisposable.
- Define un campo
RateLimiter
que se asigna desde el constructor. - Invalida el método
SendAsync
para interceptar y controlar solicitudes antes de que se envíen al servidor. - Invalida el método DisposeAsync() para eliminar la instancia de
RateLimiter
.
Si analizamos el método SendAsync
un poco más cerca, veremos lo siguiente:
- Se basa en la instancia de
RateLimiter
para obtener un objetoRateLimitLease
deAcquireAsync
. - Cuando la propiedad
lease.IsAcquired
estrue
, la solicitud se envía al servidor. - Si no, se devuelve un objeto HttpResponseMessage con un código de estado
429
, y silease
contiene un valorRetryAfter
, el encabezadoRetry-After
se establece en ese valor.
Simulación de muchas solicitudes a la vez
Para poner a prueba esta subclase DelegatingHandler
personalizada, crearemos una aplicación de consola que simula muchas solicitudes a la vez. Esta clase Program
crea un objeto HttpClient con el objeto 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})");
}
En la aplicación de consola anterior:
TokenBucketRateLimiterOptions
se configura con un límite de token de8
, con un orden de procesamiento de cola deOldestFirst
, con un límite de cola de3
, con un período de reposición de1
milisegundos, con un valor de token por período de2
y con un valor de reposición automática detrue
.- Se crea un
HttpClient
con el objetoClientSideRateLimitedHandler
que se ha configurado conTokenBucketRateLimiter
. - Para simular 100 solicitudes, Enumerable.Range crea 100 direcciones URL, cada una con un parámetro de cadena de consulta único.
- Se asignan dos objetos Task desde el método Parallel.ForEachAsync, lo que hace que las direcciones URL se dividan en dos grupos.
HttpClient
se usa para enviar una solicitudGET
a cada dirección URL, y la respuesta se escribe en la consola.- Task.WhenAll espera a que ambas tareas se completen.
Puesto que HttpClient
se ha configurado con ClientSideRateLimitedHandler
, no todas las solicitudes acceden al recurso del servidor. Para probar esta afirmación, puede ejecutar la aplicación de consola. Comprobará que se envía al servidor únicamente una fracción del número total de solicitudes, y el resto se rechaza con un código de estado HTTP 429
. Pruebe a modificar el objeto options
usado para crear el objeto TokenBucketRateLimiter
para ver cómo cambia el número de solicitudes que se envían al servidor.
Considere la siguiente salida de ejemplo:
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)
Verá que las primeras entradas registradas son siempre las respuestas 429 devueltas inmediatamente, mientras que las últimas son siempre las respuestas 200. Esto se debe a que el límite de frecuencia se encuentra en el lado cliente e impide realizar una llamada HTTP a un servidor. Esto es positivo, ya que significa que el servidor no está inundado con solicitudes. También significa que el límite de frecuencia se aplica de forma coherente en todos los clientes.
Fíjese también en que la cadena de consulta de cada dirección URL es única: examine el parámetro iteration
para ver cómo aumenta en uno en cada solicitud. Este parámetro ayuda a ilustrar que las respuestas 429 no proceden de las primeras solicitudes, sino de las solicitudes que se realizan una vez alcanzado el límite de frecuencia. Las respuestas 200 llegan más tarde, pero estas solicitudes se realizaron previamente, antes de alcanzar el límite.
Para comprender de mejor forma los distintos algoritmos de limitación de frecuencia, pruebe a reescribir este código para aceptar una implementación de RateLimiter
diferente. Además de con TokenBucketRateLimiter
, podría probar con lo siguiente:
ConcurrencyLimiter
FixedWindowRateLimiter
PartitionedRateLimiter
SlidingWindowRateLimiter
Resumen
En este artículo ha aprendido a implementar un objeto ClientSideRateLimitedHandler
personalizado. Este patrón se podría usar para implementar un cliente HTTP de frecuencia limitada para aquellos recursos que sabe que tienen límites de API. De esta manera, impedirá que la aplicación cliente realice solicitudes innecesarias al servidor y, también, que el servidor bloquee la aplicación. Además, junto con el uso de metadatos para almacenar los valores de tiempo de reintento, también puede implementar una lógica de reintento automático.