Share via


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 objeto RateLimitLease de AcquireAsync.
  • Cuando la propiedad lease.IsAcquired es true, la solicitud se envía al servidor.
  • Si no, se devuelve un objeto HttpResponseMessage con un código de estado 429, y si lease contiene un valor RetryAfter, el encabezado Retry-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 de 8, con un orden de procesamiento de cola de OldestFirst, con un límite de cola de 3, con un período de reposición de 1 milisegundos, con un valor de token por período de 2 y con un valor de reposición automática de true.
  • Se crea un HttpClient con el objeto ClientSideRateLimitedHandler que se ha configurado con TokenBucketRateLimiter.
  • 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 solicitud GET 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.

Vea también