lock: API de verrouillage simple non-bloquant pour nginx-module-lua basé sur des dictionnaires de mémoire partagée
Installation
Si vous n'avez pas configuré d'abonnement au dépôt RPM, inscrivez-vous. Ensuite, vous pouvez procéder avec les étapes suivantes.
CentOS/RHEL 7 ou Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-lock
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-lock
Pour utiliser cette bibliothèque Lua avec NGINX, assurez-vous que nginx-module-lua est installé.
Ce document décrit lua-resty-lock v0.9 publié le 17 juin 2022.
## nginx.conf
http {
# vous n'avez pas besoin de la ligne suivante si vous utilisez le
# bundle OpenResty :
lua_shared_dict my_locks 100k;
server {
...
location = /t {
content_by_lua '
local resty_lock = require "resty.lock"
for i = 1, 2 do
local lock, err = resty_lock:new("my_locks")
if not lock then
ngx.say("échec de la création du verrou : ", err)
end
local elapsed, err = lock:lock("my_key")
ngx.say("verrou : ", elapsed, ", ", err)
local ok, err = lock:unlock()
if not ok then
ngx.say("échec du déverrouillage : ", err)
end
ngx.say("déverrouillage : ", ok)
end
';
}
}
}
Description
Cette bibliothèque implémente un verrou mutex simple de manière similaire à la directive proxy_cache_lock du module ngx_proxy.
Sous le capot, cette bibliothèque utilise les dictionnaires de mémoire partagée du module ngx_lua. L'attente de verrou est non-bloquante car nous utilisons ngx.sleep de manière progressive pour interroger le verrou périodiquement.
Méthodes
Pour charger cette bibliothèque,
- vous devez spécifier le chemin de cette bibliothèque dans la directive lua_package_path de ngx_lua. Par exemple,
lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";. - vous utilisez
requirepour charger la bibliothèque dans une variable Lua locale :
local lock = require "resty.lock"
new
syntax: obj, err = lock:new(dict_name)
syntax: obj, err = lock:new(dict_name, opts)
Crée une nouvelle instance d'objet verrou en spécifiant le nom du dictionnaire partagé (créé par lua_shared_dict) et une table d'options optionnelle opts.
En cas d'échec, retourne nil et une chaîne décrivant l'erreur.
La table d'options accepte les options suivantes :
exptimeSpécifie le temps d'expiration (en secondes) pour l'entrée de verrou dans le dictionnaire de mémoire partagée. Vous pouvez spécifier jusqu'à0.001secondes. Par défaut, 30 (secondes). Même si l'invocateur ne appelle pasunlockou si l'objet détenant le verrou n'est pas GC'd, le verrou sera libéré après ce temps. Donc, un interblocage ne se produira pas même lorsque le processus de travail détenant le verrou plante.timeoutSpécifie le temps d'attente maximal (en secondes) pour les appels de méthode lock sur l'instance d'objet actuelle. Vous pouvez spécifier jusqu'à0.001secondes. Par défaut, 5 (secondes). Cette valeur d'option ne peut pas être supérieure àexptime. Ce délai est pour empêcher un appel de méthode lock d'attendre indéfiniment. Vous pouvez spécifier0pour faire en sorte que la méthode lock retourne immédiatement sans attendre si elle ne peut pas acquérir le verrou immédiatement.stepSpécifie l'étape initiale (en secondes) de sommeil lors de l'attente du verrou. Par défaut,0.001(secondes). Lorsque la méthode lock attend sur un verrou occupé, elle dort par étapes. La taille de l'étape est augmentée par un ratio (spécifié par l'optionratio) jusqu'à atteindre la limite de taille d'étape (spécifiée par l'optionmax_step).ratioSpécifie le ratio d'augmentation de l'étape. Par défaut, 2, c'est-à-dire que la taille de l'étape double à chaque itération d'attente.max_stepSpécifie la taille d'étape maximale (c'est-à-dire, l'intervalle de sommeil, en secondes) autorisée. Voir aussi les optionsstepetratio). Par défaut, 0.5 (secondes).
lock
syntax: elapsed, err = obj:lock(key)
Tente de verrouiller une clé à travers tous les processus de travail Nginx dans l'instance de serveur Nginx actuelle. Différentes clés sont différents verrous.
La longueur de la chaîne de la clé ne doit pas dépasser 65535 octets.
Retourne le temps d'attente (en secondes) si le verrou est acquis avec succès. Sinon, retourne nil et une chaîne décrivant l'erreur.
Le temps d'attente n'est pas basé sur l'horloge murale, mais plutôt sur l'addition de toutes les "étapes" d'attente. Une valeur de retour elapsed non nulle indique que quelqu'un d'autre a juste détenu ce verrou. Mais une valeur de retour zéro ne garantit pas que personne d'autre n'a juste acquis et libéré le verrou.
Lorsque cette méthode attend pour obtenir le verrou, aucun thread du système d'exploitation ne sera bloqué et le "light thread" Lua actuel sera automatiquement cédé en arrière-plan.
Il est fortement recommandé d'appeler toujours la méthode unlock() pour libérer activement le verrou dès que possible.
Si la méthode unlock() n'est jamais appelée après cet appel de méthode, le verrou sera libéré lorsque
- l'instance d'objet
resty.lockactuelle est collectée automatiquement par le GC Lua. - le
exptimepour l'entrée de verrou est atteint.
Les erreurs courantes pour cet appel de méthode sont
* "timeout"
: Le seuil de délai spécifié par l'option timeout de la méthode new est dépassé.
* "locked"
: L'instance d'objet resty.lock actuelle détient déjà un verrou (pas nécessairement de la même clé).
D'autres erreurs possibles proviennent de l'API de dictionnaire partagé de ngx_lua.
Il est nécessaire de créer différentes instances de resty.lock pour plusieurs verrous simultanés (c'est-à-dire, ceux autour de différentes clés).
unlock
syntax: ok, err = obj:unlock()
Libère le verrou détenu par l'instance d'objet resty.lock actuelle.
Retourne 1 en cas de succès. Retourne nil et une chaîne décrivant l'erreur sinon.
Si vous appelez unlock lorsqu'aucun verrou n'est actuellement détenu, l'erreur "unlocked" sera retournée.
expire
syntax: ok, err = obj:expire(timeout)
Définit le TTL du verrou détenu par l'instance d'objet resty.lock actuelle. Cela réinitialisera le délai d'attente du verrou à timeout secondes s'il est donné, sinon le timeout fourni lors de l'appel à new sera utilisé.
Notez que le timeout fourni dans cette fonction est indépendant du timeout fourni lors de l'appel à new. L'appel à expire() ne changera pas la valeur timeout spécifiée dans new et l'appel subséquent à expire(nil) utilisera toujours le nombre timeout de new.
Retourne true en cas de succès. Retourne nil et une chaîne décrivant l'erreur sinon.
Si vous appelez expire lorsqu'aucun verrou n'est actuellement détenu, l'erreur "unlocked" sera retournée.
Pour plusieurs threads légers Lua
Il est toujours une mauvaise idée de partager une seule instance d'objet resty.lock entre plusieurs "light threads" ngx_lua car l'objet lui-même est état et est vulnérable aux conditions de course. Il est fortement recommandé d'allouer toujours une instance d'objet resty.lock séparée pour chaque "light thread" qui en a besoin.
Pour les verrous de cache
Un cas d'utilisation courant pour cette bibliothèque est d'éviter le soi-disant "effet de tas de chiens", c'est-à-dire de limiter les requêtes backend concurrentes pour la même clé lorsqu'un échec de cache se produit. Cette utilisation est similaire à la directive proxy_cache_lock du module ngx_proxy.
Le flux de travail de base pour un verrou de cache est le suivant :
- Vérifiez le cache pour une correspondance avec la clé. Si un échec de cache se produit, passez à l'étape 2.
- Instanciez un objet
resty.lock, appelez la méthode lock sur la clé et vérifiez la première valeur de retour, c'est-à-dire le temps d'attente du verrou. Si c'estnil, gérez l'erreur ; sinon, passez à l'étape 3. - Vérifiez à nouveau le cache pour une correspondance. Si c'est toujours un échec, passez à l'étape 4 ; sinon, libérez le verrou en appelant unlock puis retournez la valeur mise en cache.
- Interrogez le backend (la source de données) pour la valeur, mettez le résultat dans le cache, puis libérez le verrou actuellement détenu en appelant unlock.
Voici un exemple de code assez complet qui démontre l'idée.
local resty_lock = require "resty.lock"
local cache = ngx.shared.my_cache
-- étape 1 :
local val, err = cache:get(key)
if val then
ngx.say("résultat : ", val)
return
end
if err then
return fail("échec de l'obtention de la clé depuis shm : ", err)
end
-- échec de cache !
-- étape 2 :
local lock, err = resty_lock:new("my_locks")
if not lock then
return fail("échec de la création du verrou : ", err)
end
local elapsed, err = lock:lock(key)
if not elapsed then
return fail("échec de l'acquisition du verrou : ", err)
end
-- verrou acquis avec succès !
-- étape 3 :
-- quelqu'un a peut-être déjà mis la valeur dans le cache
-- donc nous vérifions ici à nouveau :
val, err = cache:get(key)
if val then
local ok, err = lock:unlock()
if not ok then
return fail("échec du déverrouillage : ", err)
end
ngx.say("résultat : ", val)
return
end
--- étape 4 :
local val = fetch_redis(key)
if not val then
local ok, err = lock:unlock()
if not ok then
return fail("échec du déverrouillage : ", err)
end
-- FIXME : nous devrions gérer l'absence de backend plus soigneusement
-- ici, comme insérer une valeur de substitution dans le cache.
ngx.say("aucune valeur trouvée")
return
end
-- mettre à jour le cache shm avec la nouvelle valeur récupérée
local ok, err = cache:set(key, val, 1)
if not ok then
local ok, err = lock:unlock()
if not ok then
return fail("échec du déverrouillage : ", err)
end
return fail("échec de la mise à jour du cache shm : ", err)
end
local ok, err = lock:unlock()
if not ok then
return fail("échec du déverrouillage : ", err)
end
ngx.say("résultat : ", val)
Ici, nous supposons que nous utilisons le dictionnaire de mémoire partagée ngx_lua pour mettre en cache les résultats de requêtes Redis et que nous avons les configurations suivantes dans nginx.conf :
# vous voudrez peut-être changer la taille du dictionnaire pour vos cas.
lua_shared_dict my_cache 10m;
lua_shared_dict my_locks 1m;
Le dictionnaire my_cache est pour le cache de données tandis que le dictionnaire my_locks est pour resty.lock lui-même.
Plusieurs choses importantes à noter dans l'exemple ci-dessus :
- Vous devez libérer le verrou dès que possible, même lorsque d'autres erreurs non liées se produisent.
- Vous devez mettre à jour le cache avec le résultat obtenu du backend avant de libérer le verrou afin que d'autres threads attendant déjà sur le verrou puissent obtenir la valeur mise en cache lorsqu'ils obtiennent le verrou par la suite.
- Lorsque le backend ne retourne aucune valeur, nous devrions gérer le cas avec soin en insérant une valeur de substitution dans le cache.
Limitations
Certaines des fonctions API de cette bibliothèque peuvent céder. Donc, ne pas appeler ces fonctions dans des contextes de module ngx_lua où le cession est non pris en charge (pour l'instant), comme init_by_lua*,
init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua*, et log_by_lua*.
Prérequis
Voir Aussi
- le module ngx_lua : https://github.com/openresty/lua-nginx-module
- OpenResty : http://openresty.org
GitHub
Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-lock.