Dans le paysage moderne des microservices à haut débit, protéger vos APIs contre les abus, les attaques DDoS et la surcharge accidentelle n'est pas une option, c'est critique. Alors que la limitation de débit de base fonctionne bien dans les applications mono-nœud, les systèmes distribués nécessitent un état partagé pour garantir une application cohérente sur plusieurs instances. C'est ici que Redis brille, mais pour garantir véritablement l'exactitude, vous devez aller au-delà des simples opérations clé-valeur et exploiter la puissance des scripts Lua.
Le défi de l'atomicité distribuée
Imaginez que vous disposez de trois serveurs API derrière un équilibreur de charge. Un utilisateur malveillant envoie 100 requêtes par seconde, touchant le serveur A, B et C de manière round-robin. Si vous stockez les compteurs de limitation de débit localement sur chaque serveur, l'utilisateur obtient effectivement 300 requêtes par seconde autorisées, contournant ainsi votre limite de 100 req/s. Vous avez besoin d'un compteur centralisé.
Le simple incrément d'une clé dans Redis (par exemple, INCR) semble être une solution, mais il échoue sous une forte concurrence sans logique de fenêtrage appropriée. Une approche naïve pourrait consister à définir une clé avec une expiration, mais cela conduit au « problème des limites ». Un utilisateur pourrait faire 10 requêtes à la seconde 0, et 10 requêtes à la seconde 1, doublant ainsi efficacement son débit simplement en calant ses requêtes sur les bords des fenêtres temporelles. Pour résoudre cela avec précision, nous devons vérifier le compteur et définir l'expiration de manière atomique.
Pourquoi des scripts Lua ?
Redis exécute les scripts Lua de manière atomique. Cela signifie qu'une fois qu'un script commence à s'exécuter, il ne sera pas interrompu par une autre commande. Cette propriété est essentielle pour la limitation de débit car nous devons :
- Vérifier si la clé de limitation de débit existe.
- Récupérer son compteur actuel.
- Incrémenter le compteur.
- Définir l'expiration si c'est la première requête dans la fenêtre.
- Retourner l'état à l'appelant.
Faire cela avec des commandes GET et SET séparées est sujet aux conditions de concurrence. En intégrant cette logique dans un script Lua, nous nous assurons que le bloc de logique entier s'exécute comme une unité indivisible.
Mise en œuvre du compteur à fenêtre glissante
Voici une implémentation robuste d'un script Lua pour un limiteur de débit à fenêtre fixe. Ce script vérifie le nombre de requêtes dans une fenêtre temporelle spécifique (définie en millisecondes).
-- Clé pour stocker le compteur de limitation de débit
local key = KEYS[1]
-- Temps actuel en millisecondes
local now = tonumber(ARGV[1])
-- Taille de la fenêtre en millisecondes
local window = tonumber(ARGV[2])
-- Nombre maximal de requêtes autorisées
local limit = tonumber(ARGV[3])
-- Calculer le début de la fenêtre actuelle
local window_start = now - window
-- Supprimer les entrées expirées (nettoyage simple, bien que pour des fenêtres glissantes strictes, un ensemble trié soit meilleur)
-- Pour une fenêtre fixe, nous vérifions simplement si la clé existe
local count = redis.call('GET', key)
if not count then
-- Première requête dans cette fenêtre
redis.call('SET', key, 1)
redis.call('EXPIRE', key, math.ceil(window / 1000))
return {1, 1}
else
count = tonumber(count)
if count < limit then
-- Requête autorisée
redis.call('INCR', key)
return {0, count + 1}
else
-- Limite de débit dépassée
return {1, count}
end
end
Dans ce script, KEYS[1] représente l'identifiant unique du client (par exemple, son adresse IP ou sa clé API). ARGV transmet l'horodatage actuel, la durée de la fenêtre et la limite maximale autorisée. Le script retourne un tableau où le premier élément indique si la requête a été bloquée (1) ou autorisée (0), et le deuxième élément fournit le nombre de requêtes actuel.
Intégration pratique avec Python
L'intégration de cela avec une application Python Flask utilisant la bibliothèque redis-py est simple. Vous chargez le script dans le client Redis une seule fois, puis vous l'appellez par référence.
import time
import redis
# Initialiser le client Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Charger le script Lua
lua_script = """
-- (Insérer le script Lua ci-dessus ici)
"""
rate_limit_fn = r.register_script(lua_script)
def check_rate_limit(user_id, limit=10, window=60000):
now = int(time.time() * 1000)
key = f"rate_limit:{user_id}"
# Exécuter le script Lua
result = rate_limit_fn(keys=[key], args=[now, window, limit])
status, count = result
if status == 1:
# Retourner 429 Trop de requêtes
return False, count
# Retourner 200 OK
return True, count
Conclusion
La mise en œuvre de la limitation de débit distribuée avec Redis et des scripts Lua fournit une base puissante, sécurisée et haute performance pour votre infrastructure API. En garantissant l'atomicité, vous prévenez les conditions de concurrence et appliquez avec précision vos politiques de sécurité. Cette approche protège non seulement votre backend contre la surcharge, mais maintient également une expérience juste et cohérente pour les utilisateurs légitimes, quel que soit l'instance de serveur qui gère leur requête.