Limitation de débit dynamique dans OpenResty

API7.ai

January 6, 2023

OpenResty (NGINX + Lua)

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.