Dans cet article, nous examinerons certains algorithmes de limite de débit basés sur Python et Redis, de l'implémentation la plus simple à l' algorithme avancé de débit cellulaire générique (GCRA ). Nous utiliserons redis-py
pour interagir avec Redis (
pip install redis
) . Je suggĂšre de cloner mon rĂ©fĂ©rentiel pour expĂ©rimenter les limites de requĂȘte.
Limite de temps
La premiĂšre approche pour limiter le nombre de requĂȘtes par pĂ©riode consiste Ă utiliser un algorithme limitĂ© dans le temps: pour chaque clĂ© de limitation (clĂ© de dĂ©bit, quelque chose d'unique, comme un pseudo ou une adresse IP), un compteur (fixant initialement la valeur limite) et une date d'expiration sont stockĂ©s sĂ©parĂ©ment (pĂ©riode), qui diminue Ă mesure que les demandes sont reçues.
Avec Python et Redis, vous pouvez implémenter cet algorithme comme suit:
- Vérification de l'existence de la clé de contrainte.
- S'il existe, initialisez-le avec une valeur limite ( Redis SETNX ) et une date d'expiration ( Redis EXPIRE ).
- Nous diminuons cette valeur Ă chaque requĂȘte ultĂ©rieure ( Redis DECRBY ).
- Les requĂȘtes sont arrĂȘtĂ©es uniquement lorsque la valeur tombe en dessous de zĂ©ro.
- AprÚs une période de temps spécifiée, la clé est automatiquement supprimée.
from datetime import timedelta
from redis import Redis
def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
if r.setnx(key, limit):
r.expire(key, int(period.total_seconds()))
bucket_val = r.get(key)
if bucket_val and int(bucket_val) > 0:
r.decrby(key, 1)
return False
return True
Vous pouvez voir le travail de ce code en Ă©mulant la limite de 20 requĂȘtes en 30 secondes (pour ĂȘtre plus clair, j'ai mis la fonction dans un module).
import redis
from datetime import timedelta
from ratelimit import time_bucketed
r = redis.Redis(host='localhost', port=6379, db=0)
requests = 25
for i in range(requests):
if time_bucketed.request_is_limited(r, 'admin', 20, timedelta(seconds=30)):
print ('Request is limited')
else:
print ('Request is allowed')
Seules les 20 premiĂšres demandes ne seront pas limitĂ©es, aprĂšs elles vous devrez attendre 30 secondes pour que les nouvelles demandes puissent ĂȘtre renvoyĂ©es.
L'inconvĂ©nient de cette approche est qu'elle ne limite pas la frĂ©quence, mais le nombre de requĂȘtes effectuĂ©es par l'utilisateur sur une certaine pĂ©riode. Si toute la limite est immĂ©diatement Ă©puisĂ©e, vous devrez attendre la fin de la pĂ©riode.
Algorithme de seau qui fuit
Il existe une approche oĂč l'on peut limiter trĂšs soigneusement la frĂ©quence: c'est l'algorithme actuel du bucket . Le principe de fonctionnement est le mĂȘme que pour un vrai seau qui fuit: nous versons de l'eau (demandes) dans le seau jusqu'aux bords mĂȘmes, tandis qu'une partie de l'eau s'Ă©coule constamment par le trou dans le fond (gĂ©nĂ©ralement mis en Ćuvre en utilisant un processus d'arriĂšre-plan). Si la quantitĂ© d'eau versĂ©e dans le seau est supĂ©rieure Ă la quantitĂ© d'eau qui s'Ă©coule, le seau est rempli et, lorsqu'il est plein, les nouvelles demandes ne sont plus acceptĂ©es.
Comment fonctionne l'algorithme
Vous pouvez ignorer cette approche pour une solution plus élégante qui ne nécessite pas de processus séparé pour émuler la fuite: l'algorithme de débit cellulaire générique.
Algorithme de contrÎle du débit cellulaire généralisé
GCRA a Ă©tĂ© crĂ©Ă© dans l'industrie des tĂ©lĂ©communications, oĂč il est appelĂ© mode de transfert asynchrone (ATM). Il Ă©tait utilisĂ© par les gestionnaires de rĂ©seau ATM pour retarder ou rejeter des cellules - de petits paquets de donnĂ©es d'une taille fixe - qui arrivaient Ă un dĂ©bit supĂ©rieur Ă une limite spĂ©cifiĂ©e.
GCRA suit la limite restante en utilisant ce que l'on appelle l'heure d'arrivée théorique (TAT) de chaque demande:
tat = current_time + period
et limite la demande suivante si l'heure d'arrivĂ©e est infĂ©rieure au TAT actuel. Cela fonctionne bien si la frĂ©quence est de 1 demande / pĂ©riode, lorsque les demandes sont divisĂ©es en pĂ©riodes. Mais en rĂ©alitĂ©, les frĂ©quences sont gĂ©nĂ©ralement calculĂ©es comme une limite / pĂ©riode. Par exemple, si la frĂ©quence est de 10 demandes / 60 secondes, l'utilisateur peut alors faire 10 demandes dans les 6 premiĂšres secondes. Et avec une frĂ©quence de 1 requĂȘte / 6 secondes, il devra attendre 6 secondes entre les requĂȘtes.
Pour pouvoir envoyer un groupe de demandes dans un dĂ©lai court et maintenir la limitation de leur nombre pendant une pĂ©riode avec une limite> 1, chaque demande doit ĂȘtre divisĂ©e par le rapport pĂ©riode / limite, puis la prochaine heure d'arrivĂ©e thĂ©orique (
new_tat
) sera calculée différemment. Notons l'heure d'arrivée de la demande comme suit t
:
new_tat = tat + period / limit
si les demandes sont groupées (t <= tat
)new_tat = t + period / limit
si les demandes ne sont pas groupées (t > tat
)
Par conséquent:
new_tat = max(tat, t) + period / limit
La demande sera limitée, si
new_tat
supérieure à la somme du temps en cours et la période: new_tat > t + period
. Quand new_tat = tat + period / limit
nous obtenons tat + period / limit > t + period
. Par consĂ©quent, vous devez limiter les requĂȘtes uniquement lorsque tat - t > period - period / limit
.
period â period / limit
<----------------------->
--|----------------------------|--->
t TAT
Dans ce cas, nous n'avons pas besoin de mettre à jour le TAT, car les demandes limitées n'ont pas d'heure d'arrivée théorique.
Construisons maintenant la version finale du code!
from datetime import timedelta
from redis import Redis
def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
period_in_seconds = int(period.total_seconds())
t = r.time()[0]
separation = round(period_in_seconds / limit)
r.setnx(key, 0)
tat = max(int(r.get(key)), t)
if tat - t <= period_in_seconds - separation:
new_tat = max(tat, t) + separation
r.set(key, new_tat)
return False
return True
Nous avons utilisé Redis TIME car GCRA dépend du temps, et nous devons nous assurer que l'heure actuelle est cohérente sur plusieurs déploiements (les écarts d'horloge entre différentes machines peuvent conduire à des faux positifs).
Ce code dĂ©montre les performances GCRA Ă 10 requĂȘtes / 60 s.
import redis
from datetime import timedelta
from ratelimit import gcra
r = redis.Redis(host='localhost', port=6379, db=0)
requests = 10
for i in range(requests):
if gcra.request_is_limited(r, 'admin', 10, timedelta(minutes=1)):
print ('Request is limited')
else:
print ('Request is allowed')
L'algorithme ne limite pas les 10 premiĂšres requĂȘtes, mais vous devrez attendre au moins 6 secondes pour faire la requĂȘte suivante. Essayez d'exĂ©cuter le script aprĂšs un certain temps et modifiez la valeur de la limite et de la pĂ©riode (par exemple,
limit = 1
et period=timedelta(seconds=6)
).
Pour garder la mise en Ćuvre propre, je n'ai pas ajoutĂ© de verrou entre la rĂ©ception et l'envoi d'appels. Mais il est nĂ©cessaire pour plusieurs demandes avec la mĂȘme clĂ© et en mĂȘme temps. Avec Lua, vous pouvez ajouter un verrouillage en tant que gestionnaire de contexte.
def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
period_in_seconds = int(period.total_seconds())
t = r.time()[0]
separation = round(period_in_seconds / limit)
r.setnx(key, 0)
try:
with r.lock('lock:' + key, blocking_timeout=5) as lock:
tat = max(int(r.get(key)), t)
if tat - t <= period_in_seconds - separation:
new_tat = max(tat, t) + separation
r.set(key, new_tat)
return False
return True
except LockError:
return True
Le code complet est sur GitHub .