Maison >interface Web >js tutoriel >La limitation expliquée : un guide pour gérer les limites de requêtes d'API

La limitation expliquée : un guide pour gérer les limites de requêtes d'API

DDD
DDDoriginal
2024-12-05 22:29:10510parcourir

Quand devriez-vous implémenter la limitation dans votre code ?

Pour les grands projets, il est généralement préférable d'utiliser des outils comme Cloudflare Rate Limiting ou HAProxy. Ceux-ci 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 ?

  • C'est simple : vous construirez quelque chose de simple et facile à comprendre.
  • C'est économique : aucun coût supplémentaire au-delà de l'hébergement de votre serveur.
  • Cela fonctionne pour les petits projets : Tant que le trafic est faible, cela permet de garder les choses rapides et efficaces.
  • Il est réutilisable : vous pouvez le copier dans d'autres projets sans configurer de nouveaux outils ou services.

Ce que vous apprendrez

À la fin de ce guide, vous saurez comment créer un throttler de base dans TypeScript pour protéger vos API contre la surcharge. Voici ce que nous allons aborder :

  • Limites de temps configurables : chaque tentative bloquée augmente la durée du verrouillage pour éviter les abus.
  • Demand Caps : définissez un nombre maximum de requêtes autorisées. Ceci est particulièrement utile pour les API qui impliquent des services payants, comme OpenAI.
  • Stockage en mémoire : une solution simple qui fonctionne sans outils externes comme Redis, idéale pour les petits projets ou prototypes.
  • Limites par utilisateur : suivez les demandes par utilisateur à l'aide de leur adresse IPv4. Nous utiliserons SvelteKit pour récupérer facilement l’adresse IP du client grâce à sa méthode intégrée.

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 donner les bons crédits à la section Rate Limiting de Lucia.


Implémentation du régulateur

Définissons la classe Throttler :

export class Throttler {
    private storage = new Map<string, ThrottlingCounter>();

    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.

Comment ça marche

  • 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 :

    • Autoriser l'action.
    • Suivez l'utilisateur en stockant son adresse IP avec un délai d'attente initial.
  • Cas B : Utilisateur actif

    Si l'adresse IP est trouvée, cela signifie que l'utilisateur a effectué des demandes précédentes. Ici :

    • Vérifiez si le temps d'attente requis (basé sur le tableau timeoutSeconds) s'est écoulé depuis leur dernier bloc.
    • Si suffisamment de temps s'est écoulé :
    • Mettre à jour l'horodatage.
    • Incrémentez l'index de délai d'attente (plafonné au dernier index pour éviter tout débordement).
    • Sinon, refusez la demande.

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 {
    private storage = new Map<string, ThrottlingCounter>();

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

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.

Exemple de point de terminaison

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]);

Throttling Explained: A Guide to Managing API Request Limits

Remarque pour l'authentification

Lorsque vous utilisez la limitation de débit avec les systèmes de connexion, vous pourriez être confronté à ce problème :

  1. Un utilisateur se connecte, déclenchant le Throttler pour associer un délai d'attente à son IP.
  2. L'utilisateur se déconnecte ou sa session se termine (par exemple, se déconnecte immédiatement, le cookie expire avec des plantages de session et de navigateur, etc.).
  3. Lorsqu'ils tentent de se reconnecter peu de temps après, le Throttler peut toujours les bloquer, renvoyant une erreur 429 Too Many Requests.

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 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
    }
}

Et utilisez-le après une connexion réussie :

export class Throttler {
    private storage = new Map<string, ThrottlingCounter>();

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

Gestion des enregistrements IP périmés avec un nettoyage périodique

Alors que 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.

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

Un moyen très simple (mais probablement pas le meilleur) de planifier le nettoyage est d'utiliser setInterval :

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
    }
}

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.


  1. Si vous vous sentez aventureux, vous pourriez être intéressé de lire 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. ↩

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn