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é?
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:
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.
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.
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:
Caso B: Usuario activo
Si se encuentra la IP, significa que el usuario ha realizado solicitudes anteriores. Aquí:
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.
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 }); }
Al utilizar la limitación de velocidad con sistemas de inicio de sesión, es posible que enfrente este problema:
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
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.
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. ↩
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