Comment j'ai conçu un limiteur de débit à fenêtre glissante dans Redis
Notre API vidéo plantait tous les soirs à 20h00 UTC.
Ce n'était pas du trafic réel. Quelques scrapers avaient trouvé notre endpoint trending et l'avaient bombardé. Nos requêtes de base de données s'accumulaient. Les vrais utilisateurs voyaient des icônes de chargement au lieu des vidéos.
Nous avions besoin d'un système de limitation de débit. La plupart des méthodes simples aggravaient le problème.
Le problème avec les compteurs à fenêtre fixe (Fixed Window) : La plupart des gens commencent par des fenêtres fixes. Vous comptez les requêtes dans un bloc de temps défini. Si votre limite est de 100 par minute, un utilisateur peut envoyer 100 requêtes à 11:59:59 et 100 autres à 12:00:00. Cela représente 200 requêtes en une seconde. Les scrapers exploitent ces limites pour contourner vos restrictions.
La solution : La fenêtre glissante (Sliding Window). Une fenêtre glissante compte les requêtes au cours des N dernières secondes par rapport à l'instant présent. Il n'y a aucune limite fixe à exploiter.
Nous utilisons deux méthodes différentes selon nos besoins :
- Sliding Window Log (pour la précision) Nous utilisons les Sorted Sets (ZSET) de Redis pour les utilisateurs authentifiés. Nous stockons un horodatage pour chaque requête.
- Nous utilisons ZREMRANGEBYSCORE pour supprimer les anciennes entrées.
- Nous utilisons ZCARD pour compter les requêtes actuelles.
- Nous utilisons ZADD pour enregistrer les nouvelles.
Pour éviter les conditions de concurrence (race conditions), nous exécutons cette logique dans un script Lua unique. Cela rend l'opération atomique. Un seul script gère tout en un seul aller-retour vers Redis. Cela empêche deux requêtes de penser simultanément qu'elles sont toutes deux sous la limite.
- Sliding Window Counter (pour l'échelle) Pour le trafic anonyme, nous avons des millions de clés. Nous ne pouvons pas stocker chaque horodatage. À la place, nous utilisons un compteur qui pondère la fenêtre actuelle et la précédente. Cela consomme beaucoup moins de mémoire et est très efficace.
Leçons clés tirées de la production :
- Utilisez des scripts Lua. Cela garantit que votre vérification et votre écriture se produisent en une seule action.
- Émettez des en-têtes standards. Envoyez toujours X-RateLimit-Remaining et Retry-After. Cela aide les clients respectueux à ralentir automatiquement.
- Privilégiez l'ouverture en cas d'échec (fail open). Si Redis tombe, autorisez la requête. Une API surchargée est préférable à une API totalement hors service.
- Identifiez correctement les clients. Utilisez des clés API pour les utilisateurs et des adresses IP hachées pour les invités.
- Adaptez les limites aux endpoints. Un endpoint de recherche gourmand nécessite des limites plus strictes qu'un simple endpoint de liste.
Le résultat a été immédiat. Les pics du soir ont disparu. Les performances de notre base de données sont revenues à la normale.
