Zum Inhalt

lock: Einfache nicht-blockierende Lock-API für nginx-module-lua basierend auf Shared Memory-Dictionaries

Installation

Wenn Sie noch kein RPM-Repository-Abonnement eingerichtet haben, melden Sie sich an. Dann können Sie mit den folgenden Schritten fortfahren.

CentOS/RHEL 7 oder 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

Um diese Lua-Bibliothek mit NGINX zu verwenden, stellen Sie sicher, dass nginx-module-lua installiert ist.

Dieses Dokument beschreibt lua-resty-lock v0.9, das am 17. Juni 2022 veröffentlicht wurde.


## nginx.conf

http {
    # Sie benötigen die folgende Zeile nicht, wenn Sie das
    # OpenResty-Bundle verwenden:
    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("Fehler beim Erstellen des Locks: ", err)
                    end

                    local elapsed, err = lock:lock("my_key")
                    ngx.say("Lock: ", elapsed, ", ", err)

                    local ok, err = lock:unlock()
                    if not ok then
                        ngx.say("Fehler beim Entsperren: ", err)
                    end
                    ngx.say("Entsperren: ", ok)
                end
            ';
        }
    }
}

Beschreibung

Diese Bibliothek implementiert ein einfaches Mutex-Lock auf ähnliche Weise wie die proxy_cache_lock-Direktive des ngx_proxy-Moduls.

Im Hintergrund verwendet diese Bibliothek die Shared Memory-Dictionaries des ngx_lua-Moduls. Das Warten auf das Lock ist nicht-blockierend, da wir schrittweise ngx.sleep verwenden, um das Lock regelmäßig abzufragen.

Methoden

Um diese Bibliothek zu laden,

  1. müssen Sie den Pfad dieser Bibliothek in der lua_package_path-Direktive von ngx_lua angeben. Zum Beispiel: lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";.
  2. verwenden Sie require, um die Bibliothek in eine lokale Lua-Variable zu laden:
    local lock = require "resty.lock"

new

syntax: obj, err = lock:new(dict_name)

syntax: obj, err = lock:new(dict_name, opts)

Erstellt eine neue Lock-Objektinstanz, indem der Name des Shared Dictionary (erstellt durch lua_shared_dict) und eine optionale Optionen-Tabelle opts angegeben werden.

Im Falle eines Fehlers gibt es nil und eine Zeichenfolge zurück, die den Fehler beschreibt.

Die Optionen-Tabelle akzeptiert die folgenden Optionen:

  • exptime Gibt die Ablaufzeit (in Sekunden) für den Lock-Eintrag im Shared Memory-Dictionary an. Sie können bis zu 0.001 Sekunden angeben. Standardmäßig 30 (Sekunden). Selbst wenn der Aufrufer unlock nicht aufruft oder das Objekt, das den Lock hält, nicht GC'd wird, wird der Lock nach dieser Zeit freigegeben. Ein Deadlock wird also nicht auftreten, selbst wenn der Worker-Prozess, der den Lock hält, abstürzt.
  • timeout Gibt die maximale Wartezeit (in Sekunden) für die lock-Methodenaufrufe auf der aktuellen Objektinstanz an. Sie können bis zu 0.001 Sekunden angeben. Standardmäßig 5 (Sekunden). Dieser Timeout-Wert darf nicht größer als exptime sein. Dieser Timeout dient dazu, zu verhindern, dass ein lock-Methodenaufruf ewig wartet. Sie können 0 angeben, um die lock-Methode sofort zurückzugeben, ohne zu warten, wenn sie den Lock nicht sofort erwerben kann.
  • step Gibt den anfänglichen Schritt (in Sekunden) des Schlafens an, wenn auf den Lock gewartet wird. Standardmäßig 0.001 (Sekunden). Wenn die lock-Methode auf einen beschäftigten Lock wartet, schläft sie schrittweise. Die Schrittgröße wird um ein Verhältnis (angegeben durch die ratio-Option) erhöht, bis die Schrittgrößenobergrenze (angegeben durch die max_step-Option) erreicht ist.
  • ratio Gibt das Verhältnis zur Erhöhung des Schrittes an. Standardmäßig 2, das heißt, die Schrittgröße verdoppelt sich bei jeder Warteiteration.
  • max_step Gibt die maximale Schrittgröße (d.h. Schlafintervall, in Sekunden) an, die erlaubt ist. Siehe auch die Optionen step und ratio. Standardmäßig 0.5 (Sekunden).

lock

syntax: elapsed, err = obj:lock(key)

Versucht, einen Schlüssel über alle Nginx-Worker-Prozesse in der aktuellen Nginx-Serverinstanz zu sperren. Verschiedene Schlüssel sind unterschiedliche Locks.

Die Länge der Schlüsselzeichenfolge darf 65535 Bytes nicht überschreiten.

Gibt die Wartezeit (in Sekunden) zurück, wenn der Lock erfolgreich erworben wurde. Andernfalls gibt es nil und eine Zeichenfolge zurück, die den Fehler beschreibt.

Die Wartezeit stammt nicht von der Uhrzeit, sondern wird einfach durch das Addieren aller Warte "Schritte" ermittelt. Ein nicht-null elapsed Rückgabewert zeigt an, dass jemand anderes diesen Lock gerade hält. Ein null Rückgabewert kann jedoch nicht garantieren, dass niemand anders den Lock gerade erworben und wieder freigegeben hat.

Wenn diese Methode auf das Abrufen des Locks wartet, werden keine Betriebssystem-Threads blockiert, und der aktuelle Lua "Light Thread" wird automatisch im Hintergrund freigegeben.

Es wird dringend empfohlen, die unlock()-Methode immer aufzurufen, um den Lock so schnell wie möglich aktiv freizugeben.

Wenn die unlock()-Methode nach diesem Methodenaufruf niemals aufgerufen wird, wird der Lock freigegeben, wenn

  1. die aktuelle resty.lock-Objektinstanz automatisch vom Lua GC gesammelt wird.
  2. die exptime für den Lock-Eintrag erreicht ist.

Häufige Fehler für diesen Methodenaufruf sind * "timeout" : Der durch die timeout-Option der new-Methode angegebene Timeout-Schwellenwert wurde überschritten. * "locked" : Die aktuelle resty.lock-Objektinstanz hält bereits einen Lock (nicht unbedingt für denselben Schlüssel).

Andere mögliche Fehler stammen von der Shared Dictionary-API von ngx_lua.

Es ist erforderlich, für mehrere gleichzeitige Locks (d.h. für verschiedene Schlüssel) unterschiedliche resty.lock-Instanzen zu erstellen.

unlock

syntax: ok, err = obj:unlock()

Gibt den Lock frei, der von der aktuellen resty.lock-Objektinstanz gehalten wird.

Gibt 1 bei Erfolg zurück. Andernfalls gibt es nil und eine Zeichenfolge zurück, die den Fehler beschreibt.

Wenn Sie unlock aufrufen, wenn derzeit kein Lock gehalten wird, wird der Fehler "unlocked" zurückgegeben.

expire

syntax: ok, err = obj:expire(timeout)

Setzt die TTL des Locks, der von der aktuellen resty.lock-Objektinstanz gehalten wird. Dies setzt den Timeout des Locks auf timeout Sekunden zurück, wenn er angegeben wird, andernfalls wird der beim Aufruf von new angegebene timeout verwendet.

Beachten Sie, dass der timeout, der in dieser Funktion angegeben wird, unabhängig von dem timeout ist, der beim Aufruf von new angegeben wurde. Der Aufruf von expire() ändert nicht den timeout-Wert, der in new angegeben ist, und ein nachfolgender expire(nil)-Aufruf verwendet weiterhin die timeout-Zahl aus new.

Gibt true bei Erfolg zurück. Andernfalls gibt es nil und eine Zeichenfolge zurück, die den Fehler beschreibt.

Wenn Sie expire aufrufen, wenn derzeit kein Lock gehalten wird, wird der Fehler "unlocked" zurückgegeben.

Für mehrere Lua Light Threads

Es ist immer eine schlechte Idee, eine einzelne resty.lock-Objektinstanz über mehrere ngx_lua "Light Threads" zu teilen, da das Objekt selbst zustandsbehaftet ist und anfällig für Race Conditions ist. Es wird dringend empfohlen, immer eine separate resty.lock-Objektinstanz für jeden "Light Thread" zuzuweisen, der einen benötigt.

Für Cache Locks

Ein häufiger Anwendungsfall für diese Bibliothek ist es, den sogenannten "Dog-Pile-Effekt" zu vermeiden, d.h. die gleichzeitigen Backend-Abfragen für denselben Schlüssel zu begrenzen, wenn ein Cache-Miss auftritt. Diese Verwendung ist ähnlich der proxy_cache_lock-Direktive des Standard ngx_proxy-Moduls.

Der grundlegende Workflow für einen Cache-Lock ist wie folgt:

  1. Überprüfen Sie den Cache auf einen Treffer mit dem Schlüssel. Wenn ein Cache-Miss auftritt, fahren Sie mit Schritt 2 fort.
  2. Instanziieren Sie ein resty.lock-Objekt, rufen Sie die lock-Methode für den Schlüssel auf und überprüfen Sie den 1. Rückgabewert, d.h. die Lock-Wartezeit. Wenn es nil ist, behandeln Sie den Fehler; andernfalls fahren Sie mit Schritt 3 fort.
  3. Überprüfen Sie den Cache erneut auf einen Treffer. Wenn es immer noch ein Miss ist, fahren Sie mit Schritt 4 fort; andernfalls geben Sie den Lock durch Aufruf von unlock frei und geben dann den zwischengespeicherten Wert zurück.
  4. Abfragen des Backends (der Datenquelle) nach dem Wert, das Ergebnis in den Cache einfügen und dann den aktuell gehaltenen Lock durch Aufruf von unlock freigeben.

Im Folgenden finden Sie ein vollständiges Codebeispiel, das die Idee demonstriert.

    local resty_lock = require "resty.lock"
    local cache = ngx.shared.my_cache

    -- Schritt 1:
    local val, err = cache:get(key)
    if val then
        ngx.say("Ergebnis: ", val)
        return
    end

    if err then
        return fail("Fehler beim Abrufen des Schlüssels aus shm: ", err)
    end

    -- Cache-Miss!
    -- Schritt 2:
    local lock, err = resty_lock:new("my_locks")
    if not lock then
        return fail("Fehler beim Erstellen des Locks: ", err)
    end

    local elapsed, err = lock:lock(key)
    if not elapsed then
        return fail("Fehler beim Erwerben des Locks: ", err)
    end

    -- Lock erfolgreich erworben!

    -- Schritt 3:
    -- Jemand könnte den Wert bereits in den Cache eingefügt haben
    -- also überprüfen wir es hier erneut:
    val, err = cache:get(key)
    if val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("Fehler beim Entsperren: ", err)
        end

        ngx.say("Ergebnis: ", val)
        return
    end

    --- Schritt 4:
    local val = fetch_redis(key)
    if not val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("Fehler beim Entsperren: ", err)
        end

        -- FIXME: Wir sollten den Backend-Miss hier sorgfältiger behandeln
        -- wie das Einfügen eines Stub-Wertes in den Cache.

        ngx.say("Kein Wert gefunden")
        return
    end

    -- Aktualisieren Sie den shm-Cache mit dem neu abgerufenen Wert
    local ok, err = cache:set(key, val, 1)
    if not ok then
        local ok, err = lock:unlock()
        if not ok then
            return fail("Fehler beim Entsperren: ", err)
        end

        return fail("Fehler beim Aktualisieren des shm-Caches: ", err)
    end

    local ok, err = lock:unlock()
    if not ok then
        return fail("Fehler beim Entsperren: ", err)
    end

    ngx.say("Ergebnis: ", val)

Hier gehen wir davon aus, dass wir das ngx_lua Shared Memory-Dictionary verwenden, um die Redis-Abfrageergebnisse zwischenzuspeichern, und wir haben die folgenden Konfigurationen in nginx.conf:

    # Sie möchten möglicherweise die Dictionary-Größe für Ihre Fälle ändern.
    lua_shared_dict my_cache 10m;
    lua_shared_dict my_locks 1m;

Das my_cache-Dictionary dient dem Datencache, während das my_locks-Dictionary für resty.lock selbst gedacht ist.

Einige wichtige Punkte, die im obigen Beispiel zu beachten sind:

  1. Sie müssen den Lock so schnell wie möglich freigeben, selbst wenn andere nicht verwandte Fehler auftreten.
  2. Sie müssen den Cache mit dem Ergebnis, das Sie vom Backend erhalten haben, vor der Freigabe des Locks aktualisieren, damit andere Threads, die bereits auf den Lock warten, den zwischengespeicherten Wert erhalten können, wenn sie den Lock danach erhalten.
  3. Wenn das Backend überhaupt keinen Wert zurückgibt, sollten wir den Fall sorgfältig behandeln, indem wir einen Stub-Wert in den Cache einfügen.

Einschränkungen

Einige der API-Funktionen dieser Bibliothek können yielden. Rufen Sie diese Funktionen daher nicht in ngx_lua-Modulkontexten auf, in denen Yielding (noch) nicht unterstützt wird, wie init_by_lua*, init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua* und log_by_lua*.

Voraussetzungen

Siehe auch

GitHub

Sie finden zusätzliche Konfigurationstipps und Dokumentation für dieses Modul im GitHub-Repository für nginx-module-lock.