Compartir por


Límite de velocidad de un controlador HTTP en .NET

En este artículo, aprenderá a crear un controlador HTTP del lado cliente que limite el número de solicitudes que envía. Verá un HttpClient que accede al recurso "www.example.com". Las aplicaciones que dependen de ellos consumen recursos y cuando una aplicación realiza demasiadas solicitudes para un único recurso, puede provocar la contención de recursos. La contención de recursos se produce cuando demasiadas aplicaciones consumen un recurso y el recurso no puede atender todas las aplicaciones que lo solicitan. 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 velocidad es el concepto de limitar la cantidad a la que se puede acceder a un recurso. Por ejemplo, es posible que sepa que una base de datos a la que accede la aplicación puede controlar de forma segura 1000 solicitudes por minuto, pero puede que no controle mucho más que eso. Puede colocar un limitador de velocidad en la aplicación que solo permita 1000 solicitudes cada minuto y rechace más 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 un patrón común en los sistemas distribuidos, donde puede tener varias instancias de una aplicación en ejecución y desea asegurarse de que no intenten acceder a la base de datos al mismo tiempo. Hay varios algoritmos de limitación de velocidad diferentes para controlar el flujo de solicitudes.

Para usar la limitación de velocidad en .NET, hará referencia al paquete NuGet System.Threading.RateLimiting .

Implementación de una DelegatingHandler subclase

Para controlar el flujo de solicitudes, implemente una subclase personalizada DelegatingHandler . Este es un tipo de 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, implementará una subclase personalizada DelegatingHandler que limita el número de solicitudes que se pueden enviar a un único recurso. Tenga en cuenta la siguiente clase 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();
        }
    }
}

El código de C# anterior:

  • Hereda el tipo DelegatingHandler.
  • Implementa la interfaz IAsyncDisposable.
  • Define un RateLimiter campo asignado desde el constructor.
  • Invalida el SendAsync método para interceptar y controlar las solicitudes antes de que se envíen al servidor.
  • Invalida el DisposeAsync() método para eliminar la RateLimiter instancia.

Mirando un poco más cerca del SendAsync método, verá que:

  • Se basa en la instancia de RateLimiter para obtener un objeto RateLimitLease de AcquireAsync.
  • Cuando la lease.IsAcquired propiedad 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.

Emular muchas solicitudes simultáneas

Para poner a prueba esta subclase personalizada DelegatingHandler, creará una aplicación de consola que emula muchas solicitudes simultáneas. 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 emular 100 solicitudes, Enumerable.Range crea 100 direcciones URL, cada una con un parámetro de cadena de consulta único.
  • Se asignan dos Task objetos desde el Parallel.ForEachAsync método, dividiendo las direcciones URL en dos grupos.
  • HttpClient se usa para enviar una GET solicitud a cada dirección URL y la respuesta se escribe en la consola.
  • Task.WhenAll espera a que se completen ambas tareas.

Dado que HttpClient está configurado con ClientSideRateLimitedHandler, no todas las solicitudes llegarán al recurso del servidor. Puede probar esta aserción ejecutando la aplicación de consola. Verá que solo se envía una fracción del número total de solicitudes al servidor y el resto se rechaza con un código de estado HTTP de 429. Intente modificar el options objeto usado para crear 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)

Observará que las primeras entradas registradas siempre son las respuestas 429 devueltas inmediatamente, y las últimas entradas siempre son las respuestas 200. Esto se debe a que se encuentra el límite de velocidad del lado cliente y evita realizar una llamada HTTP a un servidor. Esto es algo bueno porque significa que el servidor no está inundado de solicitudes. También significa que el límite de velocidad se aplica de forma coherente en todos los clientes.

Tenga en cuenta también que la cadena de consulta de cada dirección URL es única: examine el iteration parámetro para ver que se incrementa en uno para 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 después de alcanzar el límite de velocidad. Las 200 respuestas llegan más tarde, pero estas solicitudes se realizaron anteriormente, antes de alcanzar el límite.

Para comprender mejor los distintos algoritmos de limitación de velocidad, intente volver a escribir este código para aceptar una implementación diferente RateLimiter . Además de TokenBucketRateLimiter, podría intentar:

Resumen

En este artículo ha aprendido a implementar un objeto ClientSideRateLimitedHandler personalizado. Este patrón podría usarse para implementar un cliente HTTP limitado por velocidad para los recursos que sabe que tienen límites de API. De este modo, impide que la aplicación cliente realice solicitudes innecesarias al servidor y también impide que el servidor bloquee la aplicación. Además, con el uso de metadatos para almacenar los valores de tiempo de reintento, también puede implementar la lógica de reintento automático.

Consulte también