Limitation de débit dynamique dans OpenResty
API7.ai
January 6, 2023
Dans l'article précédent, je vous ai présenté les algorithmes du seau percé
et du seau à jetons
, qui sont couramment utilisés pour gérer les pics de trafic. Nous avons également appris comment effectuer une limitation du taux de requêtes en utilisant la configuration de NGINX. Cependant, l'utilisation de la configuration NGINX reste au niveau de la praticité et il reste un long chemin à parcourir pour atteindre une véritable utilité.
Le premier problème est que la clé de limitation de taux est limitée à une plage de variables NGINX et ne peut pas être définie de manière flexible. Par exemple, il n'est pas possible de définir des seuils de limitation de vitesse différents pour différentes provinces et différents canaux clients, ce qui est une exigence courante avec NGINX.
Un autre problème plus important est que le taux ne peut pas être ajusté dynamiquement, et chaque modification nécessite un rechargement du service NGINX. Par conséquent, limiter la vitesse en fonction de différentes périodes ne peut être mis en œuvre que de manière rudimentaire via des scripts externes.
Il est important de comprendre que la technologie sert l'activité, et en même temps, l'activité stimule la technologie. À l'époque de la naissance de NGINX, il y avait peu de besoin d'ajuster la configuration de manière dynamique ; il s'agissait davantage de besoins tels que le proxy inverse, l'équilibrage de charge, une faible utilisation de la mémoire, et d'autres besoins similaires qui ont stimulé la croissance de NGINX. En termes d'architecture et de mise en œuvre technologiques, personne n'aurait pu prédire l'explosion massive de la demande pour un contrôle dynamique et granulaire dans des scénarios tels que l'Internet mobile, l'IoT et les microservices.
L'utilisation de scripts Lua par OpenResty comble le manque de NGINX dans ce domaine, en en faisant un complément efficace. C'est pourquoi OpenResty est si largement utilisé comme remplacement de NGINX. Dans les prochains articles, je continuerai à vous présenter des scénarios et des exemples plus dynamiques dans OpenResty. Commençons par voir comment utiliser OpenResty pour implémenter une limitation de taux dynamique.
Dans OpenResty, nous recommandons d'utiliser lua-resty-limit-traffic pour limiter le trafic. Il inclut limit-req
(limiter le taux de requêtes), limit-count
(limiter le nombre de requêtes) et limit-conn
(limiter les connexions concurrentes) ; et fournit limit.traffic
pour agréger ces trois méthodes.
Limiter le taux de requêtes
Commençons par examiner limit-req
, qui utilise un algorithme de seau percé pour limiter le taux de requêtes.
Dans la section précédente, nous avons brièvement introduit le code clé de l'algorithme du seau percé dans cette bibliothèque resty, et maintenant nous allons apprendre comment utiliser cette bibliothèque. Tout d'abord, regardons l'exemple de code suivant.
resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 200, 100)
local delay, err = lim:incoming("key", true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
return ngx.exit(500)
end
if delay >= 0.001 then
ngx.sleep(delay)
end'
Nous savons que lua-resty-limit-traffic
utilise un shared dict
pour stocker et compter les clés, donc nous devons déclarer l'espace de 100m
pour my_limit_req_store
avant de pouvoir utiliser limit-req
. C'est similaire pour limit-conn
et limit-count
, qui ont tous besoin d'un espace shared dict
séparé pour être distingués.
limit_req.new("my_limit_req_store", 200, 100)
La ligne de code ci-dessus est l'une des lignes de code les plus critiques. Elle signifie qu'un shared dict
appelé my_limit_req_store
est utilisé pour stocker les statistiques, et que le taux par seconde est défini à 200
, de sorte que s'il dépasse 200
mais est inférieur à 300
(cette valeur est calculée à partir de 200 + 100
), il sera mis en file d'attente, et s'il dépasse 300
, il sera rejeté.
Une fois la configuration terminée, nous devons traiter la requête du client. lim: incoming("key", true)
est là pour faire cela. incoming
a deux paramètres, que nous devons lire en détail.
Le premier paramètre, la clé spécifiée par l'utilisateur pour la limitation de taux, est une constante de chaîne dans l'exemple ci-dessus, ce qui signifie que la limitation de taux doit être uniforme pour tous les clients. Si vous souhaitez limiter le taux en fonction de différentes provinces et canaux, il est très simple d'utiliser ces deux informations comme clé, et voici le pseudo-code pour atteindre cette exigence.
local province = get_ province(ngx.var.binary_remote_addr)
local channel = ngx.req.get_headers()["channel"]
local key = province .. channel
lim:incoming(key, true)
Bien sûr, vous pouvez également personnaliser la signification de la clé et les conditions d'appel de incoming
, afin d'obtenir un effet de limitation de taux très flexible.
Examinons le deuxième paramètre de la fonction incoming
, qui est une valeur booléenne. Par défaut, elle est false
, ce qui signifie que la requête ne sera pas enregistrée dans le shared dict
pour les statistiques ; c'est juste un exercice. Si elle est définie à true
, elle aura un effet réel. Par conséquent, dans la plupart des cas, vous devrez la définir explicitement à true
.
Vous vous demandez peut-être pourquoi ce paramètre existe. Considérez un scénario où vous configurez deux instances différentes de limit-req
avec des clés différentes, une clé étant le nom d'hôte et l'autre l'adresse IP du client. Ensuite, lorsqu'une requête client est traitée, les méthodes incoming
de ces deux instances sont appelées dans l'ordre, comme indiqué par le pseudo-code suivant.
local limiter_one, err = limit_req.new("my_limit_req_store", 200, 100)
local limiter_two, err = limit_req.new("my_limit_req_store", 20, 10)
limiter_one :incoming(ngx.var.host, true)
limiter_two:incoming(ngx.var.binary_remote_addr, true)
Si la requête de l'utilisateur passe le seuil de détection de limiter_one
mais est rejetée par la détection de limiter_two
, alors l'appel de la fonction limiter_one:incoming
devrait être considéré comme un exercice et nous n'avons pas besoin de le compter.
Dans ce cas, la logique du code ci-dessus n'est pas assez rigoureuse. Nous devons faire une simulation de tous les limiteurs à l'avance afin que si un seuil de limiteur est déclenché qui peut rejeter la requête client, il puisse retourner directement.
for i = 1, n do
local lim = limiters[i]
local delay, err = lim:incoming(keys[i], i == n)
if not delay then
return nil, err
end
end
C'est ce dont il s'agit avec le deuxième argument de la fonction incoming
. Ce code est le code central du module limit.traffic
, qui est utilisé pour combiner plusieurs limiteurs de taux.
Limiter le nombre de requêtes
Examinons limit.count
, une bibliothèque qui limite le nombre de requêtes. Elle fonctionne comme l'API Rate Limiting de GitHub, qui limite le nombre de requêtes utilisateur dans une fenêtre de temps fixe. Comme d'habitude, commençons par un exemple de code.
local limit_count = require "resty.limit.count"
local lim, err = limit_count.new("my_limit_count_store", 5000, 3600)
local key = ngx.req.get_headers()["Authorization"]
local delay, remaining = lim:incoming(key, true)
Vous pouvez voir que limit.count
et limit.req
sont utilisés de manière similaire. Nous commençons par définir un shared dict
dans nginx.conf
.
lua_shared_dict my_limit_count_store 100m;
Ensuite, nous créons un nouvel objet limiteur, et enfin nous utilisons la fonction incoming
pour déterminer et traiter.
Cependant, la différence est que la deuxième valeur de retour de la fonction incoming dans limit-count
représente les appels restants, et nous pouvons ajouter des champs à l'en-tête de réponse en conséquence pour donner une meilleure indication au client.
ngx.header["X-RateLimit-Limit"] = "5000"
ngx.header["X-RateLimit-Remaining"] = remaining
Limiter le nombre de connexions concurrentes
limit.conn
est une bibliothèque pour limiter le nombre de connexions concurrentes. Elle diffère des deux bibliothèques mentionnées précédemment en ce qu'elle a une API leaving
spéciale, que je vais brièvement décrire ici.
Limiter le taux de requêtes et le nombre de requêtes, comme mentionné ci-dessus, peut être fait directement dans la phase access
. Contrairement à la limitation du nombre de connexions concurrentes, qui nécessite non seulement de déterminer si le seuil est dépassé dans la phase access
mais aussi d'appeler l'API leaving
dans la phase log
.
log_by_lua_block {
local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
local key = ctx.limit_conn_key
local conn, err = lim:leaving(key, latency)
}
Cependant, le code central de cette API est assez simple, c'est la ligne de code suivante qui décrémente le nombre de connexions de un. Si vous ne nettoyez pas dans la phase log
, le nombre de connexions continuera à augmenter et atteindra rapidement le seuil de concurrence.
local conn, err = dict:incr(key, -1)
Combinaison de limiteurs de taux
C'est la fin de l'introduction de chacune de ces trois méthodes. Enfin, voyons comment combiner limit.rate
, limit.conn
et limit.count
. Ici, nous devons utiliser la fonction combine
dans limit.traffic
.
local lim1, err = limit_req.new("my_req_store", 300, 200)
local lim2, err = limit_req.new("my_req_store", 200, 100)
local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
local limiters = {lim1, lim2, lim3}
local host = ngx.var.host
local client = ngx.var.binary_remote_addr
local keys = {host, client, client}
local delay, err = limit_traffic.combine(limiters, keys, states)
Ce code devrait être facile à comprendre avec les connaissances que vous venez d'acquérir. Le code central de la fonction combine
, que nous avons déjà mentionné dans l'analyse de limit.rate
ci-dessus, est principalement implémenté avec l'aide de la fonction drill
et de la fonction uncommit
. Cette combinaison vous permet de définir des seuils et des clés différents pour plusieurs limiteurs afin de répondre à des exigences métier plus complexes.
Résumé
Non seulement limit.traffic
prend en charge les trois limiteurs de taux mentionnés aujourd'hui, mais tant que le limiteur de taux a les API incoming
et uncommit
, il peut être géré par la fonction combine
de limit.traffic
.
Enfin, je vous laisse un devoir. Pouvez-vous écrire un exemple qui combine les limiteurs de taux de seau et de jetons que nous avons introduits précédemment ? N'hésitez pas à écrire votre réponse dans la section des commentaires pour discuter avec moi, et vous êtes également invités à partager cet article avec vos collègues et amis pour apprendre et communiquer ensemble.