Qué es Rate Limiting? Implementacion en .NET

Este es un post que llevo bastante tiempo queriendo hacer, y he querido esperar a la salida de .NET 7 para crearlo, porque la nueva versión de .NET nos trae la funcionalidad de hacer rate limiting por sí misma, lo cual simplifica las cosas un montón.



1 - Qué es el Rate Limiting?

Cuando creamos una API, en ciertas ocasiones, sobre todo cuando una api es de acceso externo, queremos limitar el uso de la API.

 

Esto se hace para evitar que se utilice más de la cuenta, un ejemplo muy claro de este funcionamiento es en aplicaciones de pago, cuando tienen una version gratuita, la de pago permite X numero de llamadas mientras que la gratuita permite muchas menos:

ejemplo rate limiting

Un ejemplo del mundo real es la API de youtube, que solo deja "consultar" 10000 unidades (videos, comentarios, etc) por día.

Lo que no es rate limiting es un filtro para limitar donde un cliente puede acceder o los permisos, para eso tenemos o bien las API Key, o los JWT.

 

 

2- Por qué utilizar rate limiting?

Uno de los motivos es directamente para limitar a los clientes, por ejemplo aceptar solo X número de llamadas, y tener una versión de pago o una versión superior la cual permite un mayor número de llamadas. 

 

Otro ejemplo que también he visto, aunque se va viendo menos y menos, es que la app o bueno la base de datos es capaz de cargar X número de llamadas a la vez, si pasas ese número, la app degrada mucho el rendimiento o directamente se cae. Una forma de asegurarnos que esto no va a pasar, es limitando el uso de la API. 

 

Y por supuesto te protege de los ataques DDoS y similares. 



Estos motivos, o una mezcla de ellos suelen ser los más comunes, un ejemplo muy común es, tenemos clientes, pueden hacer todas las llamadas que quieren, pero este todas, tiene un asterisco que dice, 10 por minuto, o cosas similares. 



3 - Implementar rate limiting en .NET

Como he dicho, en la nueva versión de .NET (versión 7) el rate limiting viene por defecto, así que si creamos o actualizamos un proyecto a net7 o superior, podremos invocar el middleware que nos permite crear la configuración para el rate limiting.

 

Para este post, he creado el rate limiting en la aplicación del curso de sistemas distribuidos Distribt la cual tiene el código disponible en GitHub.

 

Este es el diagrama completo, pero para el ejemplo únicamente debemos tener en cuenta el punto verde y un poco el de la api key que vimos en el post anterior. 

arquitectura distribt

La librería es bastante completa, y vamos a ver las diferentes opciones que nos trae, en cualquier caso, debemos poner el extension method para configurar el rate limiting middleware UseRateLimiter().

webappBuilder.Services.AddRateLimiter(options => {...})
app.UseRateLimiter();

Por cierto, pon el middleware de UseRateLimiter antes de tu primer endpoint ya que como recordarás los middlewares se ejecutan en orden. 



Antes de pasar a los diferentes tipos de rate limiting quiero explicar un poco como funciona la propia librería, vamos a ver un ejemplo muy sencillo:

webappBuilder.Services.AddRateLimiter(options =>
    options.AddConcurrencyLimiter(policyName: "concurrencyPolicy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 10;
    }));

Este ejemplo es el más sencillo, la API que tenga esta configuración sólo podrá recibir 10 llamadas simultáneas. Pero, qué pasa con el resto? 



3.1 - Colas en Rate Limiting

Una de las opciones que tenemos es habilitar una cola, a esta cola le tenemos que indicar el tamaño, y el orden (FIFO o LIFO)

webappBuilder.Services.AddRateLimiter(options =>
    options.AddConcurrencyLimiter(policyName: "concurrencyPolicy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 10;
        limiterOptions.QueueLimit = 100;
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    }));

 

3.2 - Rechazar llamadas con Rate Limiting

Pero una vez hemos llenado el número de llamadas y la cola, ¿qué pasa con las nuevas que siguen entrando? 

 

Bueno si tenemos todo “lleno” lo que vamos a hacer es rechazarlas, y podemos especificar el código de estado que queremos devolver con RejectionStatusCode.

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429; // Too many request
    options.AddConcurrencyLimiter(policyName: "concurrencyPolicy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 10;
        limiterOptions.QueueLimit = 100;
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });
});

En nuestro caso, devolvemos 429 que suele ser utilizado para indicar que se han hecho muchas requests. 



3.3 - Mensaje de respuesta en Rate Limiting

Responder el código de error, esta bien, pero un poco incompleto, lo ideal sería incluir un mensaje de error con el delegado OnRejected:

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429; // Too many request
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };
    options.AddConcurrencyLimiter(policyName: "concurrencyPolicy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 10;
        limiterOptions.QueueLimit = 100;
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });
});

 

Idealmente deberíamos normalizar la respuesta para que cumpla la normalización del resto de APIs de las que disponemos. 

api response

3.4 - Identificar quién hace la llamada en rate limiting

Para identificar quién realiza la llamada y así poder tener diferentes filtros de rate limiting lo hacemos a través de la PartitionKey, y en el siguiente apartado veremos cómo configurarla.

Pero en resumidas cuentas es lo siguiente:

partitionKey: “aquí el Identificador”




4 - Tipos de Rate limiting

Dentro del rate limiting tenemos varios tipos de configuraciones que podemos aplicar, pero no solo eso, sino que hay diferentes formas (algoritmos) de construir dicha configuración, hasta ahora hemos visto options.AddConcurrencyLimiter(...)y esta bien, pero no es la única forma, también podemos generar la configuración con  options.GlobalLimiter en el que veremos un par de ejemplos.

 

Nota: GlobalLimiter limita todo, mientras que .Add.. tiene que ser especificado por endpoit/controller, etc.

 

4.1 - Límite de concurrencia

Es el ejemplo más claro y más sencillo, simplemente permitimos X número de llamadas simultáneas, si permitimos 10 llamadas simultáneas, la número 11 será denegada

limite de concurrencia rate limitingY este sería su código:

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetConcurrencyLimiter(
            partitionKey: "aqui el identificador",
            factory: _ => new ConcurrencyLimiterOptions()
            {
                PermitLimit = 10,
                QueueLimit = 0,
                QueueProcessingOrder =  QueueProcessingOrder.OldestFirst
            }));
});

 

4.2 - Límite de depósito de tokens

El nombre de este tipo viene de su representación en el mundo real.

Tenemos un número limitado de llamadas (tokens), por ejemplo 100 y cuando una request llega lo que hace es gastar una de esos tokens; cuando hemos gastado todos, ya no podemos utilizar el servicio más.

limite de deposito de tokens rate lmiting

Aquí podemos ver el código:

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };

    options.AddTokenBucketLimiter("nombre-policy", limiterOptions =>
    {
        limiterOptions.TokensPerPeriod = 2;
        limiterOptions.TokenLimit = 100;
        limiterOptions.ReplenishmentPeriod = TimeSpan.FromMinutes(5);
    });
});

En este ejemplo tenemos un límite de 100 tokens y cada 5 minutos tenemos dos tokens disponibles.

 

4.3 - Límite de entrada Fijo

Similar al caso anterior, la diferencia es que no tenemos un límite de tokens:

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };

    options.AddFixedWindowLimiter("nombre-policy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 2;
        limiterOptions.QueueLimit = 10;
        limiterOptions.Window = TimeSpan.FromMinutes(5);
    });
});

 

4.4 - Límite de ventana deslizante

Similar al caso anterior, con la diferencia es que cada ventana tiene un número determinado de segmentos, esto quiere decir que en el siguiente ejemplo: 

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };

    
    options.AddSlidingWindowLimiter("nombre-policy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 5;
        limiterOptions.QueueLimit = 10;
        limiterOptions.SegmentsPerWindow = 5;
        limiterOptions.Window = TimeSpan.FromMinutes(5);
        

    });
});

 

Como vemos tenemos 5 llamadas por ventana y la ventana dura 5 minutos, pero a la vez, tenemos 5 segmentos, lo que hace que solo podamos ejecutar una llamada por minuto.




5 - Políticas de Rate Limiting personalizadas

Podemos crear una política que sea totalmente personalizada para nosotros, para nuestro sistema, para ello lo único que debemos hacer es crear una clase que herede  de IRateLimiterPolicy<T> donde T es la partition key que vamos a utilizar, normalmente será un string.

 

Cuando creamos la clase implementamos los miembros que necesitamos y tendremos algo como lo siguiente: 

public class DistribtRateLimiterPolicy : IRateLimiterPolicy<string>
{
    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.Request.Headers["apiKey"].ToString(),
            partition => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 10,
                Window = TimeSpan.FromMinutes(60),
            });
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } =
        (context, _) =>
        {
            context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
            return new ValueTask();
        };
}

En este caso como es Distribt y sabemos que tenemos una API Key simplemente leemos dicha key para la partition key, la cual representa a un cliente. Pero en otros servicios abiertos al público podríamos tener que si la api key existe tenemos una política, por ejemplo 10 llamadas por hora, pero si no existe tenemos simplemente una llamada por hora. 

  • Nota: esta es una clase como otra cualquiera, acepta inyección de dependencias, etc. 

 

Ahora lo que debemos hacer es cambiar un poco la configuración, ya no tenemos que utilizar services.AddRateLimiter sino que cuando utilizamos useRateLimiter le especificamos la politica.

app.UseRateLimiter();
app.MapGet("/rate-limiting-test", () =>
{
    return "Hello World!";
}).RequireRateLimiting(new DistribtRateLimiterPolicy());

 

 

6 - Rate limiting por endpoints

Es cierto que la mayoría de configuraciones con Rate Limiting se suelen hacer para la api en general, tienes X número de llamadas, donde sea, pero también podemos limitar por endpoint.

 

Para ello, en vez de utilizar app.UseRateLimiting() lo que hacemos es, en nuestra configuración, creamos una policy y luego la utilizamos con RequirerateLimiting.

webappBuilder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, token) =>
    {
        await context.HttpContext.Response.WriteAsync("muchas llamadas, por favor prueba mas tarde ");
    };

    options.AddConcurrencyLimiter("nombre-policy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 2;
        limiterOptions.QueueLimit = 0;
    });
});

....

app.MapGet("/rate-limiting-test", () =>
{
    Thread.Sleep(2 * 60 * 1000);
    return "Hello World!";
}).RequireRateLimiting("nombre-policy");

 

Un caso similar funciona si queremos deshabilitar el rate limiting, lo que debemos hacer es utilizar el filtro o extension method  DisableRateLimiting.

app.MapGet("/", () => "Hello World!").DisableRateLimiting()

 

Como he mencionado esto funciona tanto en minimal APIs como en controladores MVC normales.

En MVC utilizaremos los filter attribute [EnableRateLimiting("nombre-politica")] y [DisableRateLimiting] ya bien sea a nivel de controlador o de endpoint.

 

 

Conclusión

En este post hemos visto cómo crear un Rate Limiting en .NET

Hemos visto los diferentes tipos de Rate Limiting

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café