Pour les grands projets, il est généralement préférable d'utiliser des outils tels que Cloudflare Rate Limiting ou HAProxy. Ils sont puissants, fiables et s'occupent du gros du travail à votre place.
Mais pour les petits projets (ou si vous souhaitez apprendre comment les choses fonctionnent), vous pouvez créer votre propre limiteur de débit directement dans votre code. Pourquoi?
À la fin de ce guide, vous saurez comment créer un régulateur de base dans TypeScript pour protéger vos API contre la surcharge. Voici ce que nous allons aborder :
Ce guide est conçu pour être un point de départ pratique, parfait pour les développeurs qui souhaitent apprendre les bases sans complexité inutile. Mais il n'est pas prêt pour la production.
Avant de commencer, je souhaite attribuer les bons crédits à la section Rate Limiting de Lucia.
Définissons la classe Throttler :
export class Throttler { private storage = new Map(); constructor(private timeoutSeconds: number[]) {} }
Le constructeur Throttler accepte une liste de durées d'expiration (timeoutSeconds). Chaque fois qu'un utilisateur est bloqué, la durée augmente progressivement en fonction de cette liste. Finalement, lorsque le délai d'attente final est atteint, vous pouvez même déclencher un rappel pour interdire définitivement l'adresse IP de l'utilisateur, bien que cela dépasse le cadre de ce guide.
Voici un exemple de création d'une instance de régulateur qui bloque les utilisateurs à intervalles croissants :
const throttler = new Throttler([1, 2, 4, 8, 16]);
Cette instance bloquera les utilisateurs la première fois pendant une seconde. La deuxième fois pour deux, et ainsi de suite.
Nous utilisons une carte pour stocker les adresses IP et leurs données correspondantes. Une carte est idéale car elle gère efficacement les ajouts et suppressions fréquents.
Conseil de pro : utilisez une carte pour les données dynamiques qui changent fréquemment. Pour les données statiques et immuables, un objet est préférable. (Trou de lapin 1)
Lorsque votre point de terminaison reçoit une demande, il extrait l'adresse IP de l'utilisateur et consulte le Throttler pour déterminer si la demande doit être autorisée.
Cas A : utilisateur nouveau ou inactif
Si l’adresse IP n’est pas trouvée dans le Throttler, c’est soit la première demande de l’utilisateur, soit il est inactif depuis assez longtemps. Dans ce cas:
Cas B : Utilisateur actif
Si l'adresse IP est trouvée, cela signifie que l'utilisateur a effectué des demandes précédentes. Ici:
Dans ce dernier cas, nous devons vérifier si suffisamment de temps s'est écoulé depuis le dernier bloc. Nous savons à quels timeoutSeconds nous devons faire référence grâce à un index. Sinon, rebondissez simplement. Sinon, mettez à jour l'horodatage.
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 } }
Lors de la mise à jour de l'index, il se limite au dernier index de timeoutSeconds. Sans cela, counter.index 1 le déborderait et le prochain accès this.timeoutSeconds[counter.index] entraînerait une erreur d'exécution.
Cet exemple montre comment utiliser le Throttler pour limiter la fréquence à laquelle un utilisateur peut appeler votre API. Si l'utilisateur fait trop de requêtes, il obtiendra une erreur au lieu d'exécuter la logique principale.
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 }); }
Lorsque vous utilisez la limitation de débit avec les systèmes de connexion, vous pouvez être confronté à ce problème :
Pour éviter cela, utilisez l'ID utilisateur unique de l'utilisateur au lieu de son adresse IP pour limiter le débit. En outre, vous devez réinitialiser l'état du régulateur après une connexion réussie pour éviter les blocages inutiles.
Ajouter une méthode de réinitialisation à la classe Throttler :
export class Throttler { // ... public reset(key: string): void { this.storage.delete(key); } }
Et utilisez-le après une connexion réussie :
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
Puisque votre régulateur suit les adresses IP et les limites de débit, il est important de réfléchir à comment et quand supprimer les enregistrements IP qui ne sont plus nécessaires. Sans mécanisme de nettoyage, votre régulateur continuera à stocker les enregistrements en mémoire, ce qui pourrait entraîner des problèmes de performances au fil du temps à mesure que les données augmentent.
Pour éviter cela, vous pouvez implémenter une fonction de nettoyage qui supprime périodiquement les anciens enregistrements après une certaine période d'inactivité. Voici un exemple de la façon d'ajouter une méthode de nettoyage simple pour supprimer les entrées obsolètes du régulateur.
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) } } }
Un moyen très simple (mais probablement pas le meilleur) de planifier le nettoyage consiste à utiliser setInterval :
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]) const oneMinute = 60_000 setInterval(() => throttler.cleanup(), oneMinute)
Ce mécanisme de nettoyage permet de garantir que votre régulateur ne conserve pas indéfiniment les anciens enregistrements, garantissant ainsi l'efficacité de votre application. Bien que cette approche soit simple et facile à mettre en œuvre, elle peut nécessiter des affinements supplémentaires pour des cas d'utilisation plus complexes (par exemple, utilisation d'une planification plus avancée ou gestion d'une simultanéité élevée).
Grâce à un nettoyage périodique, vous évitez l'engorgement de la mémoire et garantissez que les utilisateurs qui n'ont pas tenté de faire des requêtes depuis un certain temps ne sont plus suivis. Il s'agit d'une première étape pour rendre votre système de limitation de débit à la fois évolutif et économe en ressources.
Si vous vous sentez aventureux, vous pourriez être intéressé de savoir comment les propriétés sont attribuées et comment elles changent. Et pourquoi pas, sur les optimisations des VM comme les caches en ligne, particulièrement favorisées par le monomorphisme. Apprécier. ↩
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3