Aller au contenu

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,

  1. 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;;";.
  2. vous utilisez require pour 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 :

  • exptime Spé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.001 secondes. Par défaut, 30 (secondes). Même si l'invocateur ne appelle pas unlock ou 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.
  • timeout Spé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.001 secondes. 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écifier 0 pour faire en sorte que la méthode lock retourne immédiatement sans attendre si elle ne peut pas acquérir le verrou immédiatement.
  • step Spé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'option ratio) jusqu'à atteindre la limite de taille d'étape (spécifiée par l'option max_step).
  • ratio Spé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_step Spécifie la taille d'étape maximale (c'est-à-dire, l'intervalle de sommeil, en secondes) autorisée. Voir aussi les options step et ratio). 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

  1. l'instance d'objet resty.lock actuelle est collectée automatiquement par le GC Lua.
  2. le exptime pour 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 :

  1. Vérifiez le cache pour une correspondance avec la clé. Si un échec de cache se produit, passez à l'étape 2.
  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'est nil, gérez l'erreur ; sinon, passez à l'étape 3.
  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.
  4. 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 :

  1. Vous devez libérer le verrou dès que possible, même lorsque d'autres erreurs non liées se produisent.
  2. 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.
  3. 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

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.