"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > Explicación de la limitación: una guía para gestionar los límites de solicitudes de API

Explicación de la limitación: una guía para gestionar los límites de solicitudes de API

Publicado el 2024-12-22
Navegar:149

¿Cuándo debería implementar la limitación en su código?

Para proyectos grandes, normalmente es mejor utilizar herramientas como Cloudflare Rate Limiting o HAProxy. Estos son potentes, confiables y se encargan del trabajo pesado por usted.

Pero para proyectos más pequeños, o si quieres aprender cómo funcionan las cosas, puedes crear tu propio limitador de velocidad directamente en tu código. ¿Por qué?

  • Es simple: crearás algo sencillo y fácil de entender.
  • Es económico: No hay costos adicionales más allá del alojamiento de tu servidor.
  • Funciona para proyectos pequeños: Mientras el tráfico sea bajo, las cosas se mantienen rápidas y eficientes.
  • Es reutilizable: puedes copiarlo en otros proyectos sin configurar nuevas herramientas o servicios.

Lo que aprenderás

Al final de esta guía, sabrá cómo crear un acelerador básico en TypeScript para proteger sus API para que no se vean abrumadas. Esto es lo que cubriremos:

  • Límites de tiempo configurables: cada intento bloqueado aumenta la duración del bloqueo para evitar abusos.
  • Límites de solicitudes: establece un número máximo de solicitudes permitidas. Esto es especialmente útil para API que involucran servicios pagos, como OpenAI.
  • Almacenamiento en memoria: una solución sencilla que funciona sin herramientas externas como Redis, ideal para proyectos pequeños o prototipos.
  • Límites por usuario: realice un seguimiento de las solicitudes por usuario utilizando su dirección IPv4. Aprovecharemos SvelteKit para recuperar fácilmente la IP del cliente con su método integrado.

Esta guía está diseñada para ser un punto de partida práctico, perfecto para desarrolladores que desean aprender los conceptos básicos sin complejidad innecesaria. Pero no está listo para producción.

Antes de comenzar, quiero darle los créditos correctos a la sección de Limitación de Tarifas de Lucía.


Implementación del acelerador

Definamos la clase Throttler:

export class Throttler {
    private storage = new Map();

    constructor(private timeoutSeconds: number[]) {}
}

El constructor Throttler acepta una lista de duraciones de tiempo de espera (timeoutSeconds). Cada vez que se bloquea a un usuario, la duración aumenta progresivamente según esta lista. Con el tiempo, cuando se alcance el tiempo de espera final, incluso podrías activar una devolución de llamada para prohibir permanentemente la IP del usuario, aunque eso está más allá del alcance de esta guía.

A continuación se muestra un ejemplo de cómo crear una instancia de acelerador que bloquea a los usuarios durante intervalos crecientes:

const throttler = new Throttler([1, 2, 4, 8, 16]);

Esta instancia bloqueará a los usuarios la primera vez durante un segundo. La segunda vez para dos, y así sucesivamente.

Utilizamos un Mapa para almacenar direcciones IP y sus datos correspondientes. Un mapa es ideal porque maneja eficientemente las adiciones y eliminaciones frecuentes.

Consejo profesional: utiliza un mapa para datos dinámicos que cambian con frecuencia. Para datos estáticos e inmutables, un objeto es mejor. (Madriguera del conejo 1)


Cuando su punto final recibe una solicitud, extrae la dirección IP del usuario y consulta el acelerador para determinar si se debe permitir la solicitud.

Cómo funciona

  • Caso A: Usuario nuevo o inactivo

    Si la IP no se encuentra en el Throttler, es la primera solicitud del usuario o ha estado inactivo durante suficiente tiempo. En este caso:

    • Permitir la acción.
    • Rastree al usuario almacenando su IP con un tiempo de espera inicial.
  • Caso B: Usuario activo

    Si se encuentra la IP, significa que el usuario ha realizado solicitudes anteriores. Aquí:

    • Compruebe si el tiempo de espera requerido (basado en la matriz timeoutSeconds) ha pasado desde su último bloque.
    • Si ha pasado suficiente tiempo:
    • Actualizar la marca de tiempo.
    • Incrementar el índice de tiempo de espera (limitado al último índice para evitar el desbordamiento).
    • Si no, rechaza la solicitud.

En este último caso, debemos verificar si ha pasado suficiente tiempo desde el último bloque. Sabemos a cuál de los timeoutSeconds debemos referir gracias a un índice. Si no, simplemente recupérese. De lo contrario, actualice la marca de tiempo.

export class Throttler {
    // ...

    public consume(key: string): boolean {
        const counter = this.storage.get(key) ?? null;
        const now = Date.now();

        // Case A
        if (counter === null) {
            // At next request, will be found.
            // The index 0 of [1, 2, 4, 8, 16] returns 1.
            // That's the amount of seconds it will have to wait.
            this.storage.set(key, {
                index: 0,
                updatedAt: now
            });
            return true; // allowed
        }

        // Case B
        const timeoutMs = this.timeoutSeconds[counter.index] * 1000;
        const allowed = now - counter.updatedAt >= timeoutMs;
        if (!allowed) {
            return false; // denied
        }

        // Allow the call, but increment timeout for following requests.
        counter.updatedAt = now;
        counter.index = Math.min(counter.index   1, this.timeoutSeconds.length - 1);
        this.storage.set(key, counter);

        return true; // allowed
    }
}

Al actualizar el índice, se limita al último índice de timeoutSeconds. Sin él, counter.index 1 lo desbordaría y el siguiente acceso a this.timeoutSeconds[counter.index] resultaría en un error de tiempo de ejecución.

Ejemplo de punto final

Este ejemplo muestra cómo usar Throttler para limitar la frecuencia con la que un usuario puede llamar a su API. Si el usuario realiza demasiadas solicitudes, recibirá un error en lugar de ejecutar la lógica principal.

const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]);

export async function GET({ getClientAddress }) {
    const IP = getClientAddress();

    if (!throttler.consume(IP)) {
        throw error(429, { message: 'Too Many Requests' });
    }

    // Read from DB, call OpenAI - do the thing.

    return new Response(null, { status: 200 });
}

Throttling Explained: A Guide to Managing API Request Limits

Nota para la autenticación

Al utilizar la limitación de velocidad con sistemas de inicio de sesión, es posible que enfrente este problema:

  1. Un usuario inicia sesión, lo que hace que Throttler asocie un tiempo de espera con su IP.
  2. El usuario cierra sesión o su sesión finaliza (por ejemplo, cierra sesión inmediatamente, la cookie caduca con la sesión y el navegador falla, etc.).
  3. Cuando intenten iniciar sesión nuevamente poco después, el acelerador aún puede bloquearlos y devolverá un error 429 Demasiadas solicitudes.

Para evitar esto, utilice el ID de usuario único del usuario en lugar de su IP para limitar la velocidad. Además, debe restablecer el estado del acelerador después de iniciar sesión correctamente para evitar bloqueos innecesarios.

Agregar un método de reinicio a la clase Throttler:

export class Throttler {
    // ...

    public reset(key: string): void {
        this.storage.delete(key);
    }
}

Y úselo después de iniciar sesión correctamente:

const user = db.get(email);

if (!throttler.consume(user.ID)) {
    throw error(429);
}

const validPassword = verifyPassword(user.password, providedPassword);
if (!validPassword) {
    throw error(401);
}

throttler.reset(user.id); // Clear throttling for the user

Gestión de registros IP obsoletos con limpieza periódica

A medida que su regulador rastrea las IP y los límites de velocidad, es importante pensar en cómo y cuándo eliminar los registros de IP que ya no son necesarios. Sin un mecanismo de limpieza, el acelerador seguirá almacenando registros en la memoria, lo que podría generar problemas de rendimiento con el tiempo a medida que los datos crezcan.

Para evitar esto, puede implementar una función de limpieza que elimine periódicamente los registros antiguos después de un cierto período de inactividad. A continuación se muestra un ejemplo de cómo agregar un método de limpieza simple para eliminar entradas obsoletas del acelerador.

export class Throttler {
    // ...

    public cleanup(): void {
        const now = Date.now()

        // Capture the keys first to avoid issues during iteration (we use .delete)
        const keys = Array.from(this.storage.keys())

        for (const key of keys) {
            const counter = this.storage.get(key)
            if (!counter) {
                // Skip if the counter is already deleted (handles concurrency issues)
                return
            }

            // If the IP is at the first timeout, remove it from storage
            if (counter.index == 0) {
                this.storage.delete(key)
                continue
            }

            // Otherwise, reduce the timeout index and update the timestamp
            counter.index -= 1
            counter.updatedAt = now
            this.storage.set(key, counter)
        }
    }
}

Una forma muy sencilla (pero probablemente no la mejor) de programar la limpieza es con setInterval:

const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300])
const oneMinute = 60_000
setInterval(() => throttler.cleanup(), oneMinute)

Este mecanismo de limpieza ayuda a garantizar que su acelerador no retenga registros antiguos indefinidamente, manteniendo su aplicación eficiente. Si bien este enfoque es simple y fácil de implementar, es posible que sea necesario perfeccionarlo para casos de uso más complejos (por ejemplo, usar una programación más avanzada o manejar una alta concurrencia).

Con la limpieza periódica, evita la sobrecarga de memoria y garantiza que los usuarios que no han intentado realizar solicitudes durante un tiempo ya no sean rastreados; este es un primer paso para hacer que su sistema de limitación de velocidad sea escalable y eficiente en recursos.


  1. Si te sientes aventurero, puede que te interese leer cómo se asignan las propiedades y cómo cambian. Además, por qué no, sobre las optimizaciones de máquinas virtuales, como los cachés en línea, que se ven particularmente favorecidos por el monomorfismo. Disfrutar. ↩

Declaración de liberación Este artículo se reproduce en: https://dev.to/didof/throttling-explained-a-guide-to-managing-api-request-limits-102a?1 Si hay alguna infracción, comuníquese con [email protected] para borrarlo
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3