Limitación de velocidad para .NET

Nos complace anunciar la compatibilidad con la limitación de velocidad integrada como parte de .NET 7. La limitación de velocidad proporciona una forma de proteger los recursos para que no abrumen su aplicación y mantener el tráfico en niveles seguros.

¿Qué es un límite de velocidad?
La limitación de velocidad es el concepto de limitar la cantidad de recursos a los que se puede acceder. Por ejemplo, sabe que su aplicación accede a una base de datos que puede gestionar de forma segura 1000 solicitudes por minuto, pero no está seguro de que pueda gestionar muchas más. Puede poner un limitador de velocidad en su aplicación que permita 1000 solicitudes por minuto y niegue más hasta que puedan acceder a la base de datos. Por lo tanto, limite la velocidad de su base de datos y permita que su aplicación maneje una cantidad segura de solicitudes sin posibles fallas de la base de datos.

Hay varios algoritmos de limitación de velocidad diferentes para controlar el flujo de solicitudes. Cubriremos cuatro de los disponibles en .NET 7.

Límites de simultaneidad
Un limitador de simultaneidad limita cuántas solicitudes simultáneas pueden acceder a un recurso. Si su límite es 10, entonces 10 solicitudes pueden acceder a un recurso al mismo tiempo y no se permitirá la 11.ª solicitud. Una vez que se completa la solicitud, el número de solicitudes permitidas aumenta a 1, cuando se completa la segunda solicitud, el número aumenta a 2, etc. Esto se hace liberando RateLimitLease, del que hablaremos más adelante.

Token Bucket Restricciones
Token Bucket es un algoritmo que recibe su nombre de la descripción de cómo funciona. Imagina tener un cubo lleno de fichas. Cuando llega una solicitud, toma un token y lo guarda para siempre. Después de una cantidad constante de tiempo, alguien vuelve a agregar una cantidad predeterminada de tokens al balde, nunca más de lo que el balde puede contener. Si el depósito está vacío, cuando llegue una solicitud, se le negará el acceso al recurso.

Como un ejemplo más concreto, suponga que el depósito puede contener 10 tokens y se agregan 2 tokens al depósito cada minuto. Cuando llega una solicitud, necesita un token, por lo que nos quedan 9, entran otras 3 solicitudes, cada una de las cuales acepta un token, lo que nos deja con 6 tokens, y un minuto después obtenemos 2 tokens nuevos, lo que nos coloca en 8. Entran 8 solicitudes y toman los tokens restantes, dejándonos con 0. Si llega otra solicitud, no se permite el acceso al recurso hasta que obtengamos más tokens, lo que sucede cada minuto. Después de 5 minutos sin solicitudes, el depósito volverá a tener los 10 tokens y no se agregarán más tokens durante los próximos minutos a menos que se soliciten más tokens.

Restricción de ventana fija
El algoritmo de ventana fija utiliza el concepto de ventana que también se utilizará en el próximo algoritmo. La ventana es la cantidad de tiempo que se aplica nuestro límite antes de pasar a la siguiente ventana. En el caso de la ventana fija, pasar a la siguiente ventana significa restablecer el límite a su punto de partida. Supongamos que hay una sala de cine con una sola sala que puede acomodar a 100 personas y la película se proyecta durante 2 horas. Cuando comienza la película, hacemos que la gente comience a hacer fila para la próxima proyección en 2 horas, hasta 100 personas en fila, y luego comenzamos a decirles que regresen otro día. Después de que termine la película de 2 horas, las líneas de 0 a 100 personas pueden ingresar al cine y comenzamos a hacer cola nuevamente. Esto es lo mismo que la ventana móvil en el algoritmo de ventana fija.

Restricciones de ventana deslizante
Los algoritmos de ventana deslizante son similares a los algoritmos de ventana fija, pero con segmentos agregados. Un segmento es parte de una ventana, si dividimos la ventana anterior de 2 horas en 4 segmentos, ahora tenemos 4 segmentos de 30 minutos. También hay un índice de segmento actual que siempre apuntará al último segmento de la ventana. Las solicitudes dentro de los 30 minutos van al segmento actual y el segmento se desliza cada ventana de 30 minutos. Si hay alguna solicitud durante el segmento sobre el que se desliza la ventana, esas solicitudes ahora se vacían y nuestro límite aumenta en esa cantidad. Si no hay solicitudes, nuestro límite sigue siendo el mismo.

Por ejemplo, usemos un algoritmo de ventana deslizante con 3 segmentos de 10 minutos y un límite de 100 solicitudes. Nuestro estado inicial es de 3 segmentos, todos con recuentos de 0, y nuestro índice de segmento actual apunta al tercer segmento.

Ventana deslizante, segmento vacío y puntero de segmento actual en el segmento 3, ventana que cubre los segmentos 1-3

En los primeros 10 minutos, recibimos 50 solicitudes, todas las cuales fueron rastreadas en el segmento 3 (nuestro índice de segmento actual). Después de que hayan transcurrido 10 minutos, deslizamos la ventana por 1 segmento y al mismo tiempo movemos el índice del segmento actual al segmento 4. Cualquier solicitud de uso en el párrafo 1 ahora se vuelve a agregar a nuestros límites. Como no hay ninguno, nuestro límite es 50 (porque 50 ya se usa en el párrafo 3).

Ventana deslizante, 50 solicitudes en el segmento 3, el puntero del segmento actual está en el segmento 4, la ventana se movió para cubrir los segmentos 2-4

Durante los siguientes 10 minutos, recibimos otras 20 solicitudes, por lo que ahora tenemos 50 para el segmento 3 y 20 para el segmento 4. Nuevamente, deslizamos la ventana después de que hayan transcurrido 10 minutos, por lo que nuestro índice de segmento actual apunta a 5, y agregamos cualquier solicitud del segmento 2 a nuestro límite.

Ventana deslizante, 50 y 20 solicitudes en los segmentos 3 y 4, puntero de segmento actual en el segmento 5, ventana que cubre los segmentos 3-5

Después de 10 minutos, deslizamos la ventana nuevamente, esta vez cuando la ventana se desliza, el índice del segmento actual es 6 y el segmento 3 (el segmento con 50 solicitudes) ahora está fuera de la ventana. Así que retiramos 50 solicitudes y las añadimos a nuestro límite, que ahora será de 80, porque el segmento 4 todavía tiene 20 en uso.

Ventana deslizante, 50 solicitudes están tachadas en el segmento 3, el puntero del segmento actual está en el segmento 6 y la ventana cubre los segmentos 4-6

¡La API del limitador de velocidad
presenta el nuevo paquete nuget System.Threading.RateLimiting en .NET 7!

Este paquete proporciona primitivas para escribir limitadores de velocidad y proporciona algunos algoritmos comunes incorporados. El tipo principal es la clase base abstracta RateLimiter.

public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
    
    
    public abstract int GetAvailablePermits();
    public abstract TimeSpan? IdleDuration {
    
     get; }

    public RateLimitLease Acquire(int permitCount = 1);
    public ValueTask<RateLimitLease> WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default);

    public void Dispose();
    public ValueTask DisposeAsync();
}

RateLimiter incluye Acquire y WaitAsync como métodos principales para intentar obtener permiso para un recurso protegido. Según la aplicación, es posible que se requiera más de 1 permiso para un recurso protegido, por lo que tanto Acquire como WaitAsync aceptan un parámetro permitCount opcional. Adquirir es un método síncrono que verificará si hay suficientes permisos disponibles y devolverá un RateLimitLease que contiene información sobre si adquirió el permiso con éxito. WaitAsync es similar a Acquire, excepto que puede admitir solicitudes de permisos en cola, que se pueden quitar de la cola en algún momento en el futuro cuando los permisos estén disponibles, por lo que es asincrónico y acepta un CancellationToken opcional para permitir la cancelación de solicitudes en cola.

RateLimitLease tiene una propiedad IsAcquired para ver si se ha adquirido una licencia. Además, RateLimitLease puede contener metadatos, como un período de reintento sugerido si falla la concesión (que se mostrará en ejemplos posteriores). Por último, RateLimitLease es un one-shot y debe publicarse cuando el código se realiza utilizando el recurso protegido. Dispose le hará saber a RateLimiter que actualice su límite en función de la cantidad de licencias adquiridas. A continuación se muestra un ejemplo del uso de 1RateLimiter para intentar obtener un recurso con 1 licencia.

RateLimiter limiter = GetLimiter();
using RateLimitLease lease = limiter.Acquire(permitCount: 1);
if (lease.IsAcquired)
{
    
    
    // Do action that is protected by limiter
}
else
{
    
    
    // Error handling or add retry logic
}

En el ejemplo anterior, estamos tratando de adquirir 1 licencia utilizando el método de Adquisición síncrona. También usamos using para garantizar que el arrendamiento se elimine cuando terminemos de usar el recurso. Luego verifique el contrato de arrendamiento para ver si se obtuvo el permiso que solicitamos, si es así, podemos usar el recurso protegido; de lo contrario, es posible que deseemos realizar algún registro o manejo de errores para notificar al usuario o la aplicación que el recurso no se usó debido a la Limitación de velocidad.

Otra forma de intentar obtener permiso es WaitAsync. Este método permite poner en cola las licencias y esperar a que las licencias estén disponibles si no están disponibles. Expliquemos el concepto de hacer cola con otro ejemplo.

RateLimiter limiter = new ConcurrencyLimiter(
    new ConcurrencyLimiterOptions(permitLimit: 2, queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 2));

// thread 1:
using RateLimitLease lease = limiter.Acquire(permitCount: 2);
if (lease.IsAcquired) {
    
     }

// thread 2:
using RateLimitLease lease = await limiter.WaitAsync(permitCount: 2);
if (lease.IsAcquired) {
    
     }

Aquí mostramos el primer ejemplo utilizando una de las implementaciones de limitación de velocidad integradas, ConcurrencyLimiter. Creamos un limitador con un límite máximo de admisión de 2 y un límite de cola de 2. Esto significa que en cualquier momento se pueden adquirir un máximo de 2 permisos, y permitimos que se pongan en cola un máximo de 2 solicitudes de permisos para llamadas WaitAsync.

El parámetro queueProcessingOrder determina el orden de procesamiento de los elementos en la cola, puede ser un valor (FIFO) o (LIFO). Un comportamiento interesante a tener en cuenta es que usar cuando la cola está llena completará la llamada en cola más antigua, pero fallará hasta que haya espacio en la cola para el elemento de cola más nuevo.QueueProcessingOrder.OldestFirstQueueProcessingOrder.NewestFirstQueueProcessingOrder.NewestFirstWaitAsyncRateLimitLease

En este ejemplo, hay 2 subprocesos que intentan adquirir una licencia. Si el subproceso 1 se ejecuta primero, adquirirá correctamente 2 permisos y el subproceso 2 de WaitAsyncin se pondrá en cola para que se libere el subproceso 1 de RateLimitLeasein. Además, si otro subproceso intenta adquirir un permiso mediante Acquire o WaitAsync, recibirá inmediatamente un RateLimitLease con propiedades iguales a false, IsAcquired porque permitLimit y queueLimit se han agotado.

Si el Subproceso 2 se ejecuta primero, obtendrá inmediatamente un valor con RateLimitLease igual a IsAcquiredtrue, y cuando el Subproceso 1 se ejecute a continuación (suponiendo que el arrendamiento en el Subproceso 2 no se haya liberado), obtendrá sincrónicamente un valor con la propiedad RateLimitLease IsAcquired igual a falso, porque Adquirir no hace cola y se agota llamando a permitLimit. WaitAsync

Hasta ahora hemos visto el ConcurrencyLimiter y hemos proporcionado otros 3 limitadores. TokenBucketRateLimiter, FixedWindowRateLimiter y todos ellos implementan su propia clase abstracta que implementa SlidingWindowRateLimiter. El método se describe junto con varias propiedades para ver configuraciones comunes en los limitadores. Se explicará después de mostrar algunos ejemplos de estos limitadores de velocidad.

ReplenishingRateLimiterRateLimiterReplenishingRateLimiterTryReplenishTryReplenish

RateLimiter limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
    queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));

using RateLimitLease lease = await limiter.WaitAsync(5);

// will complete after ~5 seconds
using RateLimitLease lease2 = await limiter.WaitAsync();

Aquí mostramos el TokenBucketRateLimiter , que es más rápido que el ConcurrencyLimiter El replenishmentPeriod es la frecuencia con la que se agregan nuevos tokens (el mismo concepto que el permiso, solo un mejor nombre en el contexto del depósito de tokens) al límite. En este ejemplo, tokensPerPeriod es 1 y replenishPeriod5 segundos, por lo que cada 5 segundos agregue 1 token tokenLimit hasta 5. Finalmente, el reabastecimiento automático se establece en verdadero, lo que significa que el limitador creará un temporizador internamente para manejar el reabastecimiento de tokens cada 5 segundos.

El desarrollador llama al limitador TryReplenish si autoReplenishment se establece en falso. ReplenishingRateLimiter Esto es útil cuando se administran varias instancias de Timer y se desea reducir la sobrecarga creando una sola instancia y administrando las llamadas de reabastecimiento usted mismo, en lugar de que cada limitador cree un Timer.

ReplenishingRateLimiter[] limiters = GetLimiters();
Timer rateLimitTimer = new Timer(static state =>
{
    
    
    var replenishingLimiters = (ReplenishingRateLimiter[])state;
    foreach (var limiter in replenishingLimiters)
    {
    
    
        limiter.TryReplenish();
    }
}, limiters, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));

FixedWindowRateLimiter tiene una opción de ventana que define cuánto tiempo tarda la ventana en actualizarse.

new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(permitLimit: 2,
    queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 1, window: TimeSpan.FromSeconds(10), autoReplenishment: true));

Además de especificar cuántos segmentos y la frecuencia de deslizamiento de la ventana, también hay una opción de SlidingWindowRateLimiter.

segmentsPerWindowwindow

new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(permitLimit: 2,
    queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 1, window: TimeSpan.FromSeconds(10), segmentsPerWindow: 5, autoReplenishment: true));

Volviendo a los metadatos antes mencionados, mostremos un ejemplo donde los metadatos pueden ser útiles.

class RateLimitedHandler : DelegatingHandler
{
    
    
    private readonly RateLimiter _rateLimiter;

    public RateLimitedHandler(RateLimiter limiter) : base(new HttpClientHandler())
    {
    
    
        _rateLimiter = limiter;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
    
    
        using RateLimitLease lease = await _rateLimiter.WaitAsync(1, cancellationToken);
        if (lease.IsAcquired)
        {
    
    
            return await base.SendAsync(request, cancellationToken);
        }
        var response = new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests);
        if (lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
    
    
            response.Headers.Add(HeaderNames.RetryAfter, ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
        }
        return response;
    }
}

RateLimiter limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
    queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));;
HttpClient client = new HttpClient(new RateLimitedHandler(limiter));
await client.GetAsync("https://example.com");

En este ejemplo, estamos configurando un HttpClient que limita la velocidad y, si no obtenemos el permiso para una solicitud, queremos devolver una solicitud HTTP fallida con un código de estado 429 (demasiadas solicitudes) en lugar de enviar una solicitud a nuestro servidor recurso Realiza una solicitud HTTP. Además, las respuestas 429 pueden incluir un encabezado "Reintentar después" para que los consumidores sepan cuándo es probable que un reintento tenga éxito. Pasamos RateLimitLease usando TryGetMetadata y . También usamos , ya que es capaz de calcular una estimación de cuándo estará disponible la cantidad solicitada de tokens, ya que sabe con qué frecuencia repone tokens. Dado que no hay forma de saber cuándo estará disponible una licencia, no proporciona ningún metadato. MetadataName.RetryAfterTokenBucketRateLimiterConcurrencyLimiterRetryAfter

MetadataName es una clase estática que proporciona varias instancias creadas previamente, que acabamos de ver, de tipo , mientras que es de tipo También hay un método estático que se puede usar para crear su propia clave de metadatos con nombre fuertemente tipado. Hay 2 sobrecargas, una para escritura fuerte con parámetros, la otra acepta una cadena de nombres de metadatos y tiene parámetros.MetadataName<T>MetadataName.RetryAfterMetadataName<TimeSpan>MetadataName.ReasonPhraseMetadataName<string>MetadataName.Create<T>(string name)RateLimitLease.TryGetMetadataMetadataName<T>out Tout object

Ahora veamos otra API que se introdujo para ayudar con escenarios más complejos, ¡PartitionedRateLimiter!

El limitador de tasa de partición
también se incluye en el paquete nuget System.Threading.RateLimiting Esta es una abstracción muy similar a una clase, excepto que acepta una instancia como argumento para sus métodos. Por ejemplo ahora: Esto es útil para escenarios en los que es posible que desee cambiar el comportamiento del límite de tasa en función del valor pasado. Esto podría ser algo así como límites de simultaneidad separados para diferentes s o escenarios más complejos, como agrupar X e Y bajo el mismo límite de simultaneidad, pero W y Z están por debajo del límite del depósito de tokens.PartitionedRateLimiter<TResource>RateLimiterTResourceAcquireAcquire(TResource resourceID, int permitCount = 1)TResourceTResource

Para ayudar al uso común, proporcionamos una forma de crear vías.

PartitionedRateLimiter<TResource>PartitionedRateLimiter.Create<TResource, TPartitionKey>(...)

enum MyPolicyEnum
{
    
    
    One,
    Two,
    Admin,
    Default
}

PartitionedRateLimiter<string> limiter = PartitionedRateLimiter.Create<string, MyPolicyEnum>(resource =>
{
    
    
    if (resource == "Policy1")
    {
    
    
        return RateLimitPartition.Create(MyPolicyEnum.One, key => new MyCustomLimiter());
    }
    else if (resource == "Policy2")
    {
    
    
        return RateLimitPartition.CreateConcurrencyLimiter(MyPolicyEnum.Two, key =>
            new ConcurrencyLimiterOptions(permitLimit: 2, queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 2));
    }
    else if (resource == "Admin")
    {
    
    
        return RateLimitPartition.CreateNoLimiter(MyPolicyEnum.Admin);
    }
    else
    {
    
    
        return RateLimitPartition.CreateTokenBucketLimiter(MyPolicyEnum.Default, key =>
            new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));
    }
});
RateLimitLease lease = limiter.Acquire(resourceID: "Policy1", permitCount: 1);

// ...

RateLimitLease lease = limiter.Acquire(resourceID: "Policy2", permitCount: 1);

// ...

RateLimitLease lease = limiter.Acquire(resourceID: "Admin", permitCount: 12345678);

// ...

RateLimitLease lease = limiter.Acquire(resourceID: "other value", permitCount: 1);

PartitionedRateLimiter.Create tiene 2 parámetros de tipo genérico, el primero representa el tipo de recurso, también será TResource en la devolución. El segundo tipo genérico es el tipo de clave de partición, en el ejemplo anterior lo usamos como nuestro tipo de clave. La clave se utiliza para distinguir un grupo de instancias con el mismo limitador, que es lo que llamamos partición. Tome un que llamamos un particionador. Esta función se llama cada vez que interactúa con la función a través de o y devuelve un de la función. Contiene un método que es como el usuario especifica el identificador que tendrá la partición y el limitador que estará asociado a ese identificador.PartitionedRateLimiter<TResource>MyPolicyEnumTResourcePartitionedRateLimiter.CreateFunc<TResource, RateLimitPartition<TPartitionKey>>PartitionedRateLimiterAcquireWaitAsyncRateLimitPartition<TKey>RateLimitPartition<TKey>Create

En el primer bloque de código anterior, comprobamos si el recurso es igual a "Política1" y, si coinciden, creamos una partición con la clave y devolvemos una fábrica para crear nuestra personalizada. Se llama a la fábrica una vez y luego se almacena en caché el limitador de velocidad, por lo que el acceso futuro a la clave utilizará la misma instancia del limitador de velocidad. MyPolicyEnum.OneRateLimiterMyPolicyEnum.One

Mirando la primera condición, cuando el recurso es igual a "Policy2", también creamos una partición, esta vez usamos el método de conveniencia para crear una. Usamos una nueva clave de partición para esta partición y especificamos las opciones que serán generado. Ahora cada uno o para "Policy2" usará el mismo .else ifCreateConcurrencyLimiterConcurrencyLimiterMyPolicyEnum.TwoConcurrencyLimiterAcquireWaitAsyncConcurrencyLimiter

Nuestra tercera condición es nuestro recurso "administrador", no queremos limitar nuestro administrador, por lo que usamos CreateNoLimiter para no aplicar ningún límite. También asignamos una clave de partición a esta partición. MyPolicyEnum.Admin

Finalmente, tenemos un respaldo para todos los demás recursos que usan instancias de TokenBucketLimiter, a las que asignamos claves. Cualquier solicitud de recursos no cubiertos por nuestras condiciones utilizará esto. En general, es una buena práctica tener un limitador de respaldo que no sea noop en caso de que no cubra todas las condiciones en el futuro o agregue un nuevo comportamiento a su aplicación. MyPolicyEnum.DefaultifTokenBucketLimiter

En el siguiente ejemplo, combinemos el PartitionedRateLimiter con nuestro HttpClient personalizado anterior. Usamos HttpRequestMessage como el tipo de recurso PartitionedRateLimiter, que es el tipo DelegatingHandler que obtuvimos en el método SendAsync. y una cadena para nuestra clave de partición, ya que dividiremos según la ruta de la URL.

PartitionedRateLimiter<HttpRequestMessage> limiter = PartitionedRateLimiter.Create<HttpRequestMessage, string>(resource =>
{
    
    
    if (resource.RequestUri?.IsLoopback)
    {
    
    
        return RateLimitPartition.CreateNoLimiter("loopback");
    }

    string[]? segments = resource.RequestUri?.Segments;
    if (segments?.Length >= 2 && segments[1] == "api/")
    {
    
    
        // segments will be [] { "/", "api/", "next_path_segment", etc.. }
        return RateLimitPartition.CreateConcurrencyLimiter(segments[2].Trim('/'), key =>
            new ConcurrencyLimiterOptions(permitLimit: 2, queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 2));
    }

    return RateLimitPartition.Create("default", key => new MyCustomLimiter());
});

class RateLimitedHandler : DelegatingHandler
{
    
    
    private readonly PartitionedRateLimiter<HttpRequestMessage> _rateLimiter;

    public RateLimitedHandler(PartitionedRateLimiter<HttpRequestMessage> limiter) : base(new HttpClientHandler())
    {
    
    
        _rateLimiter = limiter;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
    
    
        using RateLimitLease lease = await _rateLimiter.WaitAsync(request, 1, cancellationToken);
        if (lease.IsAcquired)
        {
    
    
            return await base.SendAsync(request, cancellationToken);
        }
        var response = new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests);
        if (lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
    
    
            response.Headers.Add(HeaderNames.RetryAfter, ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
        }
        return response;
    }
}

Mirando de cerca el ejemplo anterior de PartitionedRateLimiter, nuestra primera verificación es localhost, y hemos decidido que si un usuario está haciendo algo localmente, no queremos limitarlo, no usará la protección de recursos ascendente que estamos tratando de hacer. La siguiente verificación es más interesante, estamos mirando la ruta de la URL y buscando cualquier solicitud al punto final /api/. Si la solicitud coincide, tomamos una parte de la ruta y creamos una partición para esa ruta en particular. Esto significa que cualquier solicitud usará una de nuestras instancias, y cualquier solicitud usará una instancia diferente de nuestro /api/apple/*ConcurrencyLimiter/api/orange/ConcurrencyLimiter Esto se debe a que usamos diferentes claves de partición para estas solicitudes, por lo que nuestro limiter factory genera un nuevo limitador para una partición diferente. Finalmente, tenemos un límite de respaldo para cualquier solicitud que no se realice a localhost o a un punto final. /api/

También muestra la actualización de RateLimitedHandler, que ahora acepta a en lugar de a y pasa a la llamada; de lo contrario, el resto del código permanece igual. PartitionedRateLimiterRateLimiterequestWaitAsync

Hay algunas cosas que vale la pena señalar en este ejemplo. Si se realiza una gran cantidad de solicitudes únicas, podemos crear muchas particiones, lo que hará que nuestro . crear particiones ilimitadas y evitar esto si es posible. Además, tenemos nuestra clave de partición, llamada para evitar el uso de un limitador diferente en este caso, como en ./api/*PartitionedRateLimiterPartitionedRateLimiterPartitionedRateLimiter.Createsegments[2].Trim('/')Trim/api/apple /api/apple/Uri Segmentos

También es posible escribir una implementación personalizada sin usar este método. A continuación se muestra un ejemplo de una implementación personalizada que utiliza límites de simultaneidad por recurso. Así que los recursos tienen sus propios límites, tienen sus propios límites, etc. Esto tiene la ventaja de ser más flexible y potencialmente más eficiente, pero a costa de mayores costos de mantenimiento. Limitador de tasa de particionesLimitador de tasa de particiones.Createint12

public sealed class PartitionedConcurrencyLimiter : PartitionedRateLimiter<int>
{
    
    
    private ConcurrentDictionary<int, int> _keyLimits = new();
    private int _permitLimit;

    private static readonly RateLimitLease FailedLease = new Lease(null, 0, 0);

    public PartitionedConcurrencyLimiter(int permitLimit)
    {
    
    
        _permitLimit = permitLimit;
    }

    public override int GetAvailablePermits(int resourceID)
    {
    
    
        if (_keyLimits.TryGetValue(resourceID, out int value))
        {
    
    
            return value;
        }
        return 0;
    }

    protected override RateLimitLease AcquireCore(int resourceID, int permitCount)
    {
    
    
        if (_permitLimit < permitCount)
        {
    
    
            return FailedLease;
        }

        bool wasUpdated = false;
        _keyLimits.AddOrUpdate(resourceID, (key) =>
        {
    
    
            wasUpdated = true;
            return _permitLimit - permitCount;
        }, (key, currentValue) =>
        {
    
    
            if (currentValue >= permitCount)
            {
    
    
                wasUpdated = true;
                currentValue -= permitCount;
            }
            return currentValue;
        });

        if (wasUpdated)
        {
    
    
            return new Lease(this, resourceID, permitCount);
        }
        return FailedLease;
    }

    protected override ValueTask<RateLimitLease> WaitAsyncCore(int resourceID, int permitCount, CancellationToken cancellationToken)
    {
    
    
        return new ValueTask<RateLimitLease>(AcquireCore(resourceID, permitCount));
    }

    private void Release(int resourceID, int permitCount)
    {
    
    
        _keyLimits.AddOrUpdate(resourceID, _permitLimit, (key, currentValue) =>
        {
    
    
            currentValue += permitCount;
            return currentValue;
        });
    }

    private sealed class Lease : RateLimitLease
    {
    
    
        private readonly int _permitCount;
        private readonly int _resourceId;
        private PartitionedConcurrencyLimiter? _limiter;

        public Lease(PartitionedConcurrencyLimiter? limiter, int resourceId, int permitCount)
        {
    
    
            _limiter = limiter;
            _resourceId = resourceId;
            _permitCount = permitCount;
        }

        public override bool IsAcquired => _limiter is not null;

        public override IEnumerable<string> MetadataNames => throw new NotImplementedException();

        public override bool TryGetMetadata(string metadataName, out object? metadata)
        {
    
    
            throw new NotImplementedException();
        }

        protected override void Dispose(bool disposing)
        {
    
    
            if (_limiter is null)
            {
    
    
                return;
            }

            _limiter.Release(_resourceId, _permitCount);
            _limiter = null;
        }
    }
}

PartitionedRateLimiter<int> limiter = new PartitionedConcurrencyLimiter(permitLimit: 10);
// both will be successful acquisitions as they use different resource IDs
RateLimitLease lease = limiter.Acquire(resourceID: 1, permitCount: 10);
RateLimitLease lease2 = limiter.Acquire(resourceID: 2, permitCount: 7);

Esta implementación tiene algunos problemas, como nunca eliminar entradas en el diccionario, no admitir colas y lanzamientos al acceder a los metadatos, así que use esto como inspiración para implementar uno personalizado, no copie sin modificar el código. Limitador de tasa de partición

Ahora que hemos visto la API principal, veamos el middleware RateLimiting en ASP.NET Core que aprovecha estas primitivas.

Middleware de limitación de velocidad
Este middleware se proporciona a través del paquete Microsoft.AspNetCore.RateLimiting NuGet. El patrón de uso principal es configurar algunas políticas de limitación de velocidad y luego adjuntar esas políticas a sus terminales. Una política es un nombre, que es el mismo que tomó el método, donde está ahora y sigue siendo una clave definida por el usuario. También hay 4 métodos de extensión para limitadores de velocidad incorporados cuando desea configurar un solo limitador para una política sin necesidad de diferentes particiones. Func<HttpContext,

RateLimitPartition<TPartitionKey>>PartitionedRateLimiter.CreateTResourceHttpContextTPartitionKey

var app = WebApplication.Create(args);

app.UseRateLimiter(new RateLimiterOptions()
    .AddConcurrencyLimiter(policyName: "get", new ConcurrencyLimiterOptions(permitLimit: 2, queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 2))
    .AddNoLimiter(policyName: "admin")
    .AddPolicy(policyName: "post", partitioner: httpContext =>
    {
    
    
        if (!StringValues.IsNullOrEmpty(httpContext.Request.Headers["token"]))
        {
    
    
            return RateLimitPartition.CreateTokenBucketLimiter("token", key =>
                new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                    queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));
        }
        else
        {
    
    
            return RateLimitPartition.Create("default", key => new MyCustomLimiter());
        }
    }));

app.MapGet("/get", context => context.Response.WriteAsync("get")).RequireRateLimiting("get");

app.MapGet("/admin", context => context.Response.WriteAsync("admin")).RequireRateLimiting("admin").RequireAuthorization("admin");

app.MapPost("/post", context => context.Response.WriteAsync("post")).RequireRateLimiting("post");

app.Run();

Este ejemplo muestra cómo agregar middleware, configurar algunas políticas y aplicar diferentes políticas a diferentes puntos finales. Comenzando en la parte superior, agregamos el middleware a nuestra canalización de middleware mediante UseRateLimiter. A continuación, agregamos algunas políticas a nuestras opciones mediante los métodos convenientes AddConcurrencyLimiter y AddNoLimiter para 2 de estas políticas, denominadas "get" y "admin". Luego usamos el método AddPolicy que permite configurar diferentes particiones dependiendo del recurso pasado (HttpContext para middleware). Finalmente, usamos el método RequireRateLimiting en los diversos puntos finales para que el middleware de limitación de velocidad sepa qué política ejecutar en qué punto final. (tenga en cuenta que el uso anterior de RequireAuthorization /admin endpoint no hace nada en este ejemplo mínimo, imagine la autenticación y la autorización configuradas)

Hay otros 2 usos del método AddPolicy. La interfaz expone una devolución de llamada, idéntica a la que describiré a continuación, y una que toma como parámetro y regresa. La primera sobrecarga acepta una instancia, la segunda una implementación de como genérico parámetro. El parámetro genérico uno usará la inyección de dependencia para llamar al constructor y crear una instancia para usted.

IRateLimiterPolicy<TPartitionKey>OnRejectedRateLimiterOptionsGetPartitionHttpContextRateLimitPartition<TPartitionKey>AddPolicyIRateLimiterPolicyIRateLimiterPolicyIRateLimiterPolicy

public class CustomRateLimiterPolicy<string> : IRateLimiterPolicy<string>
{
    
    
    private readonly ILogger _logger;

    public CustomRateLimiterPolicy(ILogger<CustomRateLimiterPolicy<string>> logger)
    {
    
    
        _logger = logger;
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected
    {
    
    
        get => (context, lease) =>
        {
    
    
            context.HttpContext.Response.StatusCode = 429;
            _logger.LogDebug("Request rejected");
            return new ValueTask();
        };
    }

    public RateLimitPartition<string> GetPartition(HttpContext context)
    {
    
    
        if (!StringValues.IsNullOrEmpty(httpContext.Request.Headers["token"]))
        {
    
    
            return RateLimitPartition.CreateTokenBucketLimiter("token", key =>
                new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
                    queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));
        }
        else
        {
    
    
            return RateLimitPartition.Create("default", key => new MyCustomLimiter());
        }
    }
}

var app = WebApplication.Create(args);
var logger = app.Services.GetRequiredService<ILogger<CustomRateLimiterPolicy<string>>>();

app.UseRateLimiter(new RateLimitOptions()
    .AddPolicy("a", new CustomRateLimiterPolicy<string>(logger))
    .AddPolicy<CustomRateLimiterPolicy<string>>("b"));

Otras configuraciones de RateLimiterOptions incluyen RejectionStatusCode, el código de estado que se devolverá si falla la adquisición del arrendamiento y devuelve 503 de forma predeterminada. Para un uso más avanzado, también hay una función que se llamará RejectionStatusCode después de usar OnRejected y recibir OnRejectedContext como parámetro.

new RateLimiterOptions()
{
    
    
    OnRejected = (context, cancellationToken) =>
    {
    
    
        context.HttpContext.StatusCode = StatusCodes.Status429TooManyRequests;
        return new ValueTask();
    }
};

Por último, pero no menos importante, RateLimiterOptions permite configurar vía global. Si se proporciona, se ejecutará antes que cualquier política especificada en el punto final. Por ejemplo, si desea limitar su aplicación para manejar 1000 solicitudes simultáneas, independientemente de la política de punto final especificada, puede configurar y establecer la propiedad con esta configuración. Limitador de tasa de particionesRateLimiterOptions.GlobalLimiterGlobalLimiterPartitionedRateLimiterGlobalLimiter

En pocas palabras
, intente limitar la velocidad y háganos saber lo que piensa. Para la API de RateLimiting en el espacio de nombres System.Threading.RateLimiting, use el paquete nuget System.Threading.RateLimiting y proporcione comentarios sobre el repositorio de tiempo de ejecución de GitHub. Para el middleware RateLimiting, use el paquete nuget Microsoft.AspNetCore.RateLimiting y proporcione comentarios sobre el repositorio AspNetCore GitHub.

おすすめ

転載: blog.csdn.net/u014249305/article/details/125886898