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,
- 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;;";. - 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:
exptimeGibt die Ablaufzeit (in Sekunden) für den Lock-Eintrag im Shared Memory-Dictionary an. Sie können bis zu0.001Sekunden angeben. Standardmäßig 30 (Sekunden). Selbst wenn der Aufruferunlocknicht 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.timeoutGibt die maximale Wartezeit (in Sekunden) für die lock-Methodenaufrufe auf der aktuellen Objektinstanz an. Sie können bis zu0.001Sekunden angeben. Standardmäßig 5 (Sekunden). Dieser Timeout-Wert darf nicht größer alsexptimesein. Dieser Timeout dient dazu, zu verhindern, dass ein lock-Methodenaufruf ewig wartet. Sie können0angeben, um die lock-Methode sofort zurückzugeben, ohne zu warten, wenn sie den Lock nicht sofort erwerben kann.stepGibt den anfänglichen Schritt (in Sekunden) des Schlafens an, wenn auf den Lock gewartet wird. Standardmäßig0.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 dieratio-Option) erhöht, bis die Schrittgrößenobergrenze (angegeben durch diemax_step-Option) erreicht ist.ratioGibt das Verhältnis zur Erhöhung des Schrittes an. Standardmäßig 2, das heißt, die Schrittgröße verdoppelt sich bei jeder Warteiteration.max_stepGibt die maximale Schrittgröße (d.h. Schlafintervall, in Sekunden) an, die erlaubt ist. Siehe auch die Optionenstepundratio. 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
- die aktuelle
resty.lock-Objektinstanz automatisch vom Lua GC gesammelt wird. - die
exptimefü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:
- Überprüfen Sie den Cache auf einen Treffer mit dem Schlüssel. Wenn ein Cache-Miss auftritt, fahren Sie mit Schritt 2 fort.
- 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 esnilist, behandeln Sie den Fehler; andernfalls fahren Sie mit Schritt 3 fort. - Ü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.
- 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:
- Sie müssen den Lock so schnell wie möglich freigeben, selbst wenn andere nicht verwandte Fehler auftreten.
- 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.
- 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
- das ngx_lua-Modul: https://github.com/openresty/lua-nginx-module
- OpenResty: http://openresty.org
GitHub
Sie finden zusätzliche Konfigurationstipps und Dokumentation für dieses Modul im GitHub-Repository für nginx-module-lock.