mlcache: Schicht-Caching-Bibliothek für nginx-module-lua
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-mlcache
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-mlcache
Um diese Lua-Bibliothek mit NGINX zu verwenden, stellen Sie sicher, dass nginx-module-lua installiert ist.
Dieses Dokument beschreibt lua-resty-mlcache v2.7.0, das am 14. Februar 2024 veröffentlicht wurde.
Schnelles und automatisiertes Schicht-Caching für OpenResty.
Diese Bibliothek kann als Key/Value-Speicher verwendet werden, um skalare Lua-Typen und Tabellen zu cachen, und kombiniert die Leistung der [lua_shared_dict] API und [lua-resty-lrucache], was zu einer äußerst leistungsfähigen und flexiblen Caching-Lösung führt.
Funktionen:
- Caching und negatives Caching mit TTLs.
- Eingebauter Mutex über [lua-resty-lock], um Dog-Pile-Effekte auf Ihre Datenbank/Backend bei Cache-Fehlermeldungen zu verhindern.
- Eingebaute Inter-Worker-Kommunikation zur Propagierung von Cache-Invalidierungen und zur Erlaubnis für Worker, ihre L1 (lua-resty-lrucache) Caches bei Änderungen zu aktualisieren (
set(),delete()). - Unterstützung für geteilte Treffer- und Fehlschlag-Caching-Warteschlangen.
- Mehrere isolierte Instanzen können erstellt werden, um verschiedene Datentypen zu halten, während sie auf den gleichen
lua_shared_dictL2 Cache angewiesen sind.
Illustration der verschiedenen Caching-Ebenen, die in dieser Bibliothek integriert sind:
┌─────────────────────────────────────────────────┐
│ Nginx │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │worker │ │worker │ │worker │ │
│ L1 │ │ │ │ │ │ │
│ │ Lua cache │ │ Lua cache │ │ Lua cache │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ │ │
│ L2 │ lua_shared_dict │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ mutex │
│ ▼ │
│ ┌──────────────────┐ │
│ │ callback │ │
│ └────────┬─────────┘ │
└───────────────────────────┼─────────────────────┘
│
L3 │ I/O fetch
▼
Datenbank, API, DNS, Disk, jede I/O...
Die Cache-Ebenenhierarchie ist:
- L1: Least-Recently-Used Lua VM Cache unter Verwendung von [lua-resty-lrucache].
Bietet die schnellste Suche, wenn sie gefüllt ist, und vermeidet das Erschöpfen des Arbeitsspeichers der Worker.
- L2: lua_shared_dict Speicherzone, die von allen Workern geteilt wird. Diese Ebene wird nur zugegriffen, wenn L1 ein Fehlschlag war, und verhindert, dass Worker den L3 Cache anfordern.
- L3: eine benutzerdefinierte Funktion, die nur von einem einzelnen Worker ausgeführt wird, um den Dog-Pile-Effekt auf Ihre Datenbank/Backend zu vermeiden (über [lua-resty-lock]). Werte, die über L3 abgerufen werden, werden im L2 Cache für andere Worker gespeichert.
Diese Bibliothek wurde auf der OpenResty Con 2018 vorgestellt. Siehe den Abschnitt Ressourcen für eine Aufzeichnung des Vortrags.
Synopsis
## nginx.conf
http {
# Sie müssen die folgende Zeile nicht konfigurieren, wenn Sie
# LuaRocks oder opm verwenden.
# 'on' ist bereits der Standard für diese Direktive. Wenn 'off', wird der L1-Cache
# ineffektiv sein, da die Lua-VM für jede
# Anfrage neu erstellt wird. Dies ist während der Entwicklung in Ordnung, aber stellen Sie sicher, dass in der Produktion 'on' ist.
lua_code_cache on;
lua_shared_dict cache_dict 1m;
init_by_lua_block {
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_dict", {
lru_size = 500, -- Größe des L1 (Lua VM) Caches
ttl = 3600, -- 1h TTL für Treffer
neg_ttl = 30, -- 30s TTL für Fehlschläge
})
if err then
end
-- Wir legen unsere Instanz in die globale Tabelle für die Kürze in
-- diesem Beispiel, aber bevorzugen Sie einen Upvalue zu einem Ihrer Module
-- wie von ngx_lua empfohlen
_G.cache = cache
}
server {
listen 8080;
location / {
content_by_lua_block {
local function callback(username)
-- dies wird nur *einmal* ausgeführt, bis der Schlüssel abläuft, also
-- führen Sie kostspielige Operationen wie das Verbinden mit einem Remote
-- Backend hier aus. z.B.: rufen Sie einen MySQL-Server in diesem Callback auf
return db:get_user(username) -- { name = "John Doe", email = "[email protected]" }
end
-- dieser Aufruf versucht L1 und L2, bevor der Callback (L3) ausgeführt wird
-- der zurückgegebene Wert wird dann in L2 und L1
-- für die nächste Anfrage gespeichert.
local user, err = cache:get("my_key", nil, callback, "jdoe")
ngx.say(user.name) -- "John Doe"
}
}
}
}
Methoden
new
syntax: cache, err = mlcache.new(name, shm, opts?)
Erstellt eine neue mlcache-Instanz. Bei einem Fehler gibt es nil und einen String zurück, der den Fehler beschreibt.
Das erste Argument name ist ein beliebiger Name Ihrer Wahl für diesen Cache und muss ein String sein. Jede mlcache-Instanz namespace die Werte, die sie hält, entsprechend ihrem Namen, sodass mehrere Instanzen mit dem gleichen Namen die gleichen Daten teilen.
Das zweite Argument shm ist der Name der lua_shared_dict shared memory zone. Mehrere Instanzen von mlcache können dasselbe shm verwenden (Werte werden namespace).
Das dritte Argument opts ist optional. Wenn angegeben, muss es eine Tabelle sein, die die gewünschten Optionen für diese Instanz enthält. Die möglichen Optionen sind:
lru_size: eine Zahl, die die Größe des zugrunde liegenden L1-Caches definiert (lua-resty-lrucache-Instanz). Diese Größe ist die maximale Anzahl von Elementen, die der L1-Cache halten kann. Standard:100.ttl: eine Zahl, die den Ablaufzeitraum der zwischengespeicherten Werte angibt. Die Einheit ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3. Einttlvon0bedeutet, dass die zwischengespeicherten Werte niemals ablaufen. Standard:30.neg_ttl: eine Zahl, die den Ablaufzeitraum der zwischengespeicherten Fehlschläge angibt (wenn der L3-Callbacknilzurückgibt). Die Einheit ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3. Einneg_ttlvon0bedeutet, dass die zwischengespeicherten Fehlschläge niemals ablaufen. Standard:5.resurrect_ttl: optional Zahl. Wenn angegeben, wird die mlcache-Instanz versuchen, veraltete Werte wiederherzustellen, wenn der L3-Callbacknil, errzurückgibt (weiche Fehler). Weitere Details zu dieser Option finden Sie im Abschnitt get(). Die Einheit ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3.lru: optional. Eine lua-resty-lrucache-Instanz Ihrer Wahl. Wenn angegeben, wird mlcache keinen LRU instanziieren. Man kann diesen Wert verwenden, um dieresty.lrucache.pureffiImplementierung von lua-resty-lrucache zu verwenden, wenn gewünscht.shm_set_tries: die Anzahl der Versuche für die lua_shared_dictset()-Operation. Wenn dielua_shared_dictvoll ist, versucht sie, bis zu 30 Elemente aus ihrer Warteschlange freizugeben. Wenn der Wert, der gesetzt werden soll, viel größer ist als der freigegebene Speicher, ermöglicht diese Option mlcache, die Operation erneut zu versuchen (und mehr Slots freizugeben), bis die maximale Anzahl von Versuchen erreicht ist oder genügend Speicher freigegeben wurde, damit der Wert passt. Standard:3.shm_miss: optional String. Der Name einerlua_shared_dict. Wenn angegeben, werden Fehlschläge (Callbacks, dienilzurückgeben) in diesem separatenlua_shared_dictzwischengespeichert. Dies ist nützlich, um sicherzustellen, dass eine große Anzahl von Cache-Fehlschlägen (z.B. ausgelöst durch böswillige Clients) nicht zu viele zwischengespeicherte Elemente (Treffer) aus dem inshmangegebenenlua_shared_dictverdrängt.shm_locks: optional String. Der Name einerlua_shared_dict. Wenn angegeben, verwendet lua-resty-lock dieses Shared Dict, um seine Locks zu speichern. Diese Option kann helfen, Cache-Churning zu reduzieren: Wenn der L2-Cache (shm) voll ist, purgt jede Einfügung (wie Locks, die durch gleichzeitige Zugriffe ausgelöst werden, die L3-Callbacks auslösen) die ältesten 30 zugegriffenen Elemente. Diese gelöschten Elemente sind höchstwahrscheinlich zuvor (und wertvolle) zwischengespeicherte Werte. Durch die Isolierung von Locks in einem separaten Shared Dict können Workloads, die Cache-Churning erfahren, diesen Effekt mildern.resty_lock_opts: optional Tabelle. Optionen für [lua-resty-lock] Instanzen. Wenn mlcache den L3-Callback ausführt, verwendet es lua-resty-lock, um sicherzustellen, dass ein einzelner Worker den bereitgestellten Callback ausführt.ipc_shm: optional String. Wenn Sie set(), delete() oder purge() verwenden möchten, müssen Sie einen IPC (Inter-Process Communication)-Mechanismus bereitstellen, damit Worker ihre L1-Caches synchronisieren und ungültig machen können. Dieses Modul bündelt eine "fertige" IPC-Bibliothek, und Sie können sie aktivieren, indem Sie in dieser Option einen dediziertenlua_shared_dictangeben. Mehrere mlcache-Instanzen können dasselbe Shared Dict verwenden (Ereignisse werden namespace), aber kein anderer Akteur als mlcache sollte damit herumspielen.ipc: optional Tabelle. Wie die oben genannteipc_shm-Option, aber ermöglicht Ihnen die Verwendung der IPC-Bibliothek Ihrer Wahl, um Inter-Worker-Ereignisse zu propagieren.l1_serializer: optional Funktion. Ihre Signatur und akzeptierten Werte sind im get() Methode dokumentiert, zusammen mit einem Beispiel. Wenn angegeben, wird diese Funktion jedes Mal aufgerufen, wenn ein Wert vom L2-Cache in den L1 (Worker Lua VM) befördert wird. Diese Funktion kann beliebige Serialisierungen des zwischengespeicherten Elements durchführen, um es in jedes Lua-Objekt vor der Speicherung im L1-Cache zu transformieren. Sie kann somit vermeiden, dass Ihre Anwendung solche Transformationen bei jeder Anfrage wiederholen muss, wie z.B. das Erstellen von Tabellen, cdata-Objekten, das Laden neuer Lua-Codes usw.
Beispiel:
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000, -- bis zu 1000 Elemente im L1-Cache (Lua VM) halten
ttl = 3600, -- speichert skalare Typen und Tabellen für 1h
neg_ttl = 60 -- speichert nil-Werte für 60s
})
if not cache then
error("konnte mlcache nicht erstellen: " .. err)
end
Sie können mehrere mlcache-Instanzen erstellen, die auf derselben zugrunde liegenden lua_shared_dict shared memory zone basieren:
local mlcache = require "mlcache"
local cache_1 = mlcache.new("cache_1", "cache_shared_dict", { lru_size = 100 })
local cache_2 = mlcache.new("cache_2", "cache_shared_dict", { lru_size = 1e5 })
Im obigen Beispiel ist cache_1 ideal, um einige sehr große Werte zu halten. cache_2 kann verwendet werden, um eine große Anzahl kleiner Werte zu halten. Beide Instanzen werden auf dasselbe shm angewiesen: lua_shared_dict cache_shared_dict 2048m;. Selbst wenn Sie in beiden Caches identische Schlüssel verwenden, werden sie sich nicht gegenseitig stören, da sie jeweils einen anderen Namespace haben.
Dieses andere Beispiel instanziiert ein mlcache unter Verwendung des gebündelten IPC-Moduls für Inter-Worker-Invalidierungsereignisse (damit wir set(), delete() und purge() verwenden können):
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache_with_ipc", "cache_shared_dict", {
lru_size = 1000,
ipc_shm = "ipc_shared_dict"
})
Hinweis: Damit der L1-Cache effektiv ist, stellen Sie sicher, dass lua_code_cache aktiviert ist (was der Standard ist). Wenn Sie diese Direktive während der Entwicklung deaktivieren, funktioniert mlcache, aber das L1-Caching wird ineffektiv sein, da für jede Anfrage eine neue Lua-VM erstellt wird.
get
syntax: value, err, hit_level = cache:get(key, opts?, callback?, ...)
Führt eine Cache-Abfrage durch. Dies ist die primäre und effizienteste Methode dieses Moduls. Ein typisches Muster ist, nicht set() aufzurufen und get() die gesamte Arbeit erledigen zu lassen.
Wenn diese Methode erfolgreich ist, gibt sie value zurück und err wird auf nil gesetzt. Da nil-Werte aus dem L3-Callback zwischengespeichert werden können (d.h. "negatives Caching"), kann value nil sein, obwohl es bereits zwischengespeichert ist. Daher muss man beachten, den zweiten Rückgabewert err zu überprüfen, um festzustellen, ob diese Methode erfolgreich war oder nicht.
Der dritte Rückgabewert ist eine Zahl, die gesetzt wird, wenn kein Fehler aufgetreten ist. Sie zeigt die Ebene an, auf der der Wert abgerufen wurde: 1 für L1, 2 für L2 und 3 für L3.
Wenn jedoch ein Fehler auftritt, gibt diese Methode nil in value und einen String, der den Fehler in err beschreibt, zurück.
Das erste Argument key ist ein String. Jeder Wert muss unter einem eindeutigen Schlüssel gespeichert werden.
Das zweite Argument opts ist optional. Wenn angegeben, muss es eine Tabelle sein, die die gewünschten Optionen für diesen Schlüssel enthält. Diese Optionen haben Vorrang vor den Optionen der Instanz:
ttl: eine Zahl, die den Ablaufzeitraum der zwischengespeicherten Werte angibt. Die Einheit ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3. Einttlvon0bedeutet, dass die zwischengespeicherten Werte niemals ablaufen. Standard: wird von der Instanz geerbt.neg_ttl: eine Zahl, die den Ablaufzeitraum der zwischengespeicherten Fehlschläge angibt (wenn der L3-Callbacknilzurückgibt). Die Einheit ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3. Einneg_ttlvon0bedeutet, dass die zwischengespeicherten Fehlschläge niemals ablaufen. Standard: wird von der Instanz geerbt.resurrect_ttl: optionale Zahl. Wenn angegeben, wirdget()versuchen, veraltete Werte wiederherzustellen, wenn Fehler auftreten. Fehler, die vom L3-Callback zurückgegeben werden (nil, err), werden als Fehlschläge beim Abrufen/Aktualisieren eines Wertes betrachtet. Wenn solche Rückgabewerte vom Callback vonget()gesehen werden und der veraltete Wert noch im Speicher ist, wirdget()den veralteten Wert fürresurrect_ttlSekunden wiederherstellen. Der Fehler, der vonget()zurückgegeben wird, wird auf WARN-Niveau protokolliert, aber nicht an den Aufrufer zurückgegeben. Schließlich wird der Rückgabewerthit_level4sein, um anzuzeigen, dass das servierte Element veraltet ist. Wennresurrect_ttlerreicht ist, wirdget()erneut versuchen, den Callback auszuführen. Wenn der Callback dann erneut einen Fehler zurückgibt, wird der Wert erneut wiederhergestellt usw. Wenn der Callback erfolgreich ist, wird der Wert aktualisiert und nicht mehr als veraltet markiert. Aufgrund aktueller Einschränkungen innerhalb des LRU-Cache-Moduls wirdhit_level1sein, wenn veraltete Werte in den L1-Cache befördert und von dort abgerufen werden. Lua-Fehler, die vom Callback ausgelöst werden, lösen keine Wiederherstellung aus und werden vonget()wie gewohnt zurückgegeben (nil, err). Wenn mehrere Worker eine Zeitüberschreitung haben, während sie auf den Worker warten, der den Callback ausführt (z.B. weil der Datenspeicher eine Zeitüberschreitung hat), werden Benutzer dieser Option einen leichten Unterschied im Vergleich zum traditionellen Verhalten vonget()feststellen. Anstattnil, errzurückzugeben (was auf eine Lock-Zeitüberschreitung hinweist), gibtget()den veralteten Wert (wenn verfügbar), keinen Fehler undhit_levelzurück, der4sein wird. Der Wert wird jedoch nicht wiederhergestellt (da ein anderer Worker den Callback weiterhin ausführt). Die Einheit für diese Option ist Sekunden, akzeptiert jedoch auch Bruchzahlen wie0.3. Diese Option muss größer als0sein, um zu verhindern, dass veraltete Werte unbegrenzt zwischengespeichert werden. Standard: wird von der Instanz geerbt.shm_set_tries: die Anzahl der Versuche für die lua_shared_dictset()-Operation. Wenn dielua_shared_dictvoll ist, versucht sie, bis zu 30 Elemente aus ihrer Warteschlange freizugeben. Wenn der Wert, der gesetzt werden soll, viel größer ist als der freigegebene Speicher, ermöglicht diese Option mlcache, die Operation erneut zu versuchen (und mehr Slots freizugeben), bis die maximale Anzahl von Versuchen erreicht ist oder genügend Speicher freigegeben wurde, damit der Wert passt. Standard: wird von der Instanz geerbt.l1_serializer: optionale Funktion. Ihre Signatur und akzeptierten Werte sind im get() Methode dokumentiert, zusammen mit einem Beispiel. Wenn angegeben, wird diese Funktion jedes Mal aufgerufen, wenn ein Wert vom L2-Cache in den L1 (Worker Lua VM) befördert wird. Diese Funktion kann beliebige Serialisierungen des zwischengespeicherten Elements durchführen, um es in jedes Lua-Objekt vor der Speicherung im L1-Cache zu transformieren. Sie kann somit vermeiden, dass Ihre Anwendung solche Transformationen bei jeder Anfrage wiederholen muss, wie z.B. das Erstellen von Tabellen, cdata-Objekten, das Laden neuer Lua-Codes usw. Standard: wird von der Instanz geerbt.resty_lock_opts: optionale Tabelle. Wenn angegeben, überschreibt die Instanzresty_lock_optsfür die aktuelleget()-Abfrage. Standard: wird von der Instanz geerbt.
Das dritte Argument callback ist optional. Wenn angegeben, muss es eine Funktion sein, deren Signatur und Rückgabewerte im folgenden Beispiel dokumentiert sind:
-- arg1, arg2 und arg3 sind Argumente, die vom Callback aus den
-- `get()` variadischen Argumenten weitergeleitet werden, so:
-- cache:get(key, opts, callback, arg1, arg2, arg3)
local function callback(arg1, arg2, arg3)
-- I/O Lookup-Logik
-- ...
-- value: der Wert, der zwischengespeichert werden soll (Lua-Skalar oder Tabelle)
-- err: wenn nicht `nil`, wird get() abgebrochen, was `value` und `err` zurückgibt
-- ttl: überschreibt ttl für diesen Wert
-- Wenn als `ttl >= 0` zurückgegeben, wird es die Instanz
-- (oder Option) `ttl` oder `neg_ttl` überschreiben.
-- Wenn als `ttl < 0` zurückgegeben, wird `value` von get() zurückgegeben,
-- aber nicht zwischengespeichert. Dieser Rückgabewert wird ignoriert, wenn er keine Zahl ist.
return value, err, ttl
end
Die bereitgestellte callback-Funktion darf Lua-Fehler auslösen, da sie im geschützten Modus ausgeführt wird. Solche Fehler, die aus dem Callback ausgelöst werden, werden als Strings im zweiten Rückgabewert err zurückgegeben.
Wenn callback nicht bereitgestellt wird, wird get() dennoch den angeforderten Schlüssel im L1- und L2-Cache abfragen und ihn zurückgeben, wenn er gefunden wird. Im Fall, dass kein Wert im Cache gefunden wird und kein Callback bereitgestellt wird, gibt get() nil, nil, -1 zurück, wobei -1 einen Cache-Fehlschlag (kein Wert) bedeutet. Dies ist nicht mit Rückgabewerten wie nil, nil, 1 zu verwechseln, wobei 1 einen negativ zwischengespeicherten Artikel im L1 (zwischengespeichertes nil) anzeigt.
Das Nichtbereitstellen einer callback-Funktion ermöglicht die Implementierung von Cache-Abfragemustern, die garantiert CPU-basiert sind, um ein konstanteres, reibungsloseres Latenz-Ende zu erzielen (z.B. mit Werten, die in Hintergrundtimern über set() aktualisiert werden).
local value, err, hit_lvl = cache:get("key")
if value == nil then
if err ~= nil then
-- Fehler
elseif hit_lvl == -1 then
-- Fehlschlag (kein Wert)
else
-- negativer Treffer (zwischengespeicherter `nil`-Wert)
end
end
Wenn ein Callback bereitgestellt wird, folgt get() der folgenden Logik:
- Abfrage des L1-Caches (lua-resty-lrucache-Instanz). Dieser Cache lebt in der Lua-VM und ist daher der effizienteste, den man abfragen kann.
- Wenn der L1-Cache den Wert hat, geben Sie ihn zurück.
- Wenn der L1-Cache den Wert nicht hat (L1-Fehlschlag), fahren Sie fort.
- Abfrage des L2-Caches (
lua_shared_dictSpeicherzone). Dieser Cache wird von allen Workern geteilt und ist fast so effizient wie der L1-Cache. Er erfordert jedoch die Serialisierung von gespeicherten Lua-Tabellen.- Wenn der L2-Cache den Wert hat, geben Sie ihn zurück.
- Wenn
l1_serializergesetzt ist, führen Sie es aus und befördern Sie den resultierenden Wert in den L1-Cache. - Wenn nicht, befördern Sie den Wert direkt so, wie er im L1-Cache ist.
- Wenn
- Wenn der L2-Cache den Wert nicht hat (L2-Fehlschlag), fahren Sie fort.
- Wenn der L2-Cache den Wert hat, geben Sie ihn zurück.
- Erstellen Sie einen [lua-resty-lock] und stellen Sie sicher, dass ein einzelner Worker den Callback ausführt (andere Worker, die versuchen, auf denselben Wert zuzugreifen, warten).
- Ein einzelner Worker führt den L3-Callback aus (z.B. führt eine Datenbankabfrage durch)
- Der Callback war erfolgreich und gibt einen Wert zurück: Der Wert wird im L2-Cache gesetzt und dann im L1-Cache (so wie er ist, standardmäßig, oder wie von
l1_serializerzurückgegeben, wenn angegeben). - Der Callback ist fehlgeschlagen und hat
nil, errzurückgegeben: a. Wennresurrect_ttlangegeben ist und der veraltete Wert noch verfügbar ist, stellen Sie ihn im L2-Cache wieder her und befördern Sie ihn in den L1. b. Andernfalls gibtget()nil, errzurück. - Andere Worker, die versucht haben, auf denselben Wert zuzugreifen, aber gewartet haben, werden entsperrt und lesen den Wert aus dem L2-Cache (sie führen nicht den L3-Callback aus) und geben ihn zurück.
Wenn kein Callback bereitgestellt wird, führt get() nur die Schritte 1. und 2. aus.
Hier ist ein vollständiges Beispiel für die Verwendung:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000,
ttl = 3600,
neg_ttl = 60
})
local function fetch_user(user_id)
local user, err = db:query_user(user_id)
if err then
-- in diesem Fall gibt get() `nil` + `err` zurück
return nil, err
end
return user -- Tabelle oder nil
end
local user_id = 3
local user, err = cache:get("users:" .. user_id, nil, fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "konnte Benutzer nicht abrufen: ", err)
return
end
-- `user` könnte eine Tabelle sein, könnte aber auch `nil` sein (existiert nicht)
-- unabhängig davon wird es zwischengespeichert und nachfolgende Aufrufe von get()
-- geben den zwischengespeicherten Wert für bis zu `ttl` oder `neg_ttl` zurück.
if user then
ngx.say("Benutzer existiert: ", user.name)
else
ngx.say("Benutzer existiert nicht")
end
Dieses zweite Beispiel ist ähnlich wie das obige, aber hier wenden wir einige Transformationen auf den abgerufenen user-Datensatz an, bevor wir ihn über den l1_serializer-Callback zwischenspeichern:
-- Unser l1_serializer, der aufgerufen wird, wenn ein Wert vom L2 in den L1 befördert wird
--
-- Seine Signatur erhält ein einzelnes Argument: das Element, das von
-- einem L2-Treffer zurückgegeben wird. Daher kann dieses Argument niemals `nil` sein. Das Ergebnis wird in
-- dem L1-Cache gespeichert, aber es kann nicht `nil` sein.
--
-- Diese Funktion kann `nil` und einen String zurückgeben, der einen Fehler beschreibt, der
-- an den Aufrufer von `get()` weitergegeben wird. Sie läuft auch im geschützten Modus
-- und wird jeden Lua-Fehler melden.
local function load_code(user_row)
if user_row.custom_code ~= nil then
local f, err = loadstring(user_row.raw_lua_code)
if not f then
-- in diesem Fall wird nichts im Cache gespeichert (als ob der L3
-- Callback fehlgeschlagen wäre)
return nil, "Fehler beim Kompilieren des benutzerdefinierten Codes: " .. err
end
user_row.f = f
end
return user_row
end
local user, err = cache:get("users:" .. user_id,
{ l1_serializer = load_code },
fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "konnte Benutzer nicht abrufen: ", err)
return
end
-- jetzt können wir eine Funktion aufrufen, die bereits einmal geladen wurde, beim Eintritt
-- in den L1-Cache (Lua VM)
user.f()
get_bulk
syntax: res, err = cache:get_bulk(bulk, opts?)
Führt mehrere get() Abfragen auf einmal (in Bulk) durch. Jede dieser Abfragen, die einen L3-Callback erfordert, wird gleichzeitig in einem Pool von ngx.thread ausgeführt.
Das erste Argument bulk ist eine Tabelle, die n Operationen enthält.
Das zweite Argument opts ist optional. Wenn angegeben, muss es eine Tabelle sein, die die Optionen für diese Bulk-Abfrage enthält. Die möglichen Optionen sind:
concurrency: eine Zahl größer als0. Gibt die Anzahl der Threads an, die gleichzeitig die L3-Callbacks für diese Bulk-Abfrage ausführen werden. Eine Parallelität von3mit 6 auszuführenden Callbacks bedeutet, dass jeder Thread 2 Callbacks ausführt. Eine Parallelität von1mit 6 Callbacks bedeutet, dass ein einzelner Thread alle 6 Callbacks ausführt. Bei einer Parallelität von6und 1 Callback wird ein einzelner Thread den Callback ausführen. Standard:3.
Im Erfolgsfall gibt diese Methode res zurück, eine Tabelle, die die Ergebnisse jeder Abfrage enthält, und keinen Fehler.
Im Fehlerfall gibt diese Methode nil plus einen String zurück, der den Fehler beschreibt.
Alle Abfrageoperationen, die von dieser Methode durchgeführt werden, integrieren sich vollständig in andere Operationen, die von anderen Methoden und Nginx-Workern gleichzeitig durchgeführt werden (z.B. L1/L2 Treffer/Fehlschläge Speicherung, L3 Callback Mutex, usw.).
Das bulk-Argument ist eine Tabelle, die ein bestimmtes Layout haben muss (im folgenden Beispiel dokumentiert). Es kann manuell erstellt oder über die new_bulk() Hilfsmethode erstellt werden.
Ähnlich hat die res-Tabelle auch ein bestimmtes Layout. Sie kann manuell durchlaufen werden oder über den each_bulk_res Iterator-Helfer.
Beispiel:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
cache:get("key_c", nil, function() return nil end)
local res, err = cache:get_bulk({
-- Bulk-Layout:
-- Schlüssel Optionen L3-Callback Callback-Argument
"key_a", { ttl = 60 }, function() return "hello" end, nil,
"key_b", nil, function() return "world" end, nil,
"key_c", nil, function() return "bye" end, nil,
n = 3 -- geben Sie die Anzahl der Operationen an
}, { concurrency = 3 })
if err then
ngx.log(ngx.ERR, "konnte Bulk-Abfrage nicht ausführen: ", err)
return
end
-- res-Layout:
-- data, "err", hit_lvl }
for i = 1, res.n, 3 do
local data = res[i]
local err = res[i + 1]
local hit_lvl = res[i + 2]
if not err then
ngx.say("data: ", data, ", hit_lvl: ", hit_lvl)
end
end
Das obige Beispiel würde die folgende Ausgabe erzeugen:
data: hello, hit_lvl: 3
data: world, hit_lvl: 3
data: nil, hit_lvl: 1
Beachten Sie, dass der key_c bereits im Cache war, der Callback, der "bye" zurückgibt, niemals ausgeführt wurde, da get_bulk() den Wert aus L1 abgerufen hat, wie durch den hit_lvl-Wert angezeigt.
Hinweis: Im Gegensatz zu get() erlaubt diese Methode nur, ein einzelnes Argument für jeden Callback der Abfrage anzugeben.
new_bulk
syntax: bulk = mlcache.new_bulk(n_lookups?)
Erstellt eine Tabelle, die Lookup-Operationen für die get_bulk() Funktion enthält. Es ist nicht erforderlich, diese Funktion zu verwenden, um eine Bulk-Abfragetabelle zu erstellen, aber sie bietet eine schöne Abstraktion.
Das erste und einzige Argument n_lookups ist optional, und wenn angegeben, ist es eine Zahl, die die Anzahl der Abfragen angibt, die dieser Bulk letztendlich enthalten wird, sodass die zugrunde liegende Tabelle zur Optimierung vorab zugewiesen wird.
Diese Funktion gibt eine Tabelle bulk zurück, die noch keine Lookup-Operationen enthält. Lookups werden zu einer bulk-Tabelle hinzugefügt, indem bulk:add(key, opts?, cb, arg?) aufgerufen wird:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local bulk = mlcache.new_bulk(3)
bulk:add("key_a", { ttl = 60 }, function(n) return n * n, 42)
bulk:add("key_b", nil, function(str) return str end, "hello")
bulk:add("key_c", nil, function() return nil end)
local res, err = cache:get_bulk(bulk)
each_bulk_res
syntax: iter, res, i = mlcache.each_bulk_res(res)
Bietet eine Abstraktion, um über eine get_bulk() res Rückgabetabelle zu iterieren. Es ist nicht erforderlich, diese Methode zu verwenden, um über eine res-Tabelle zu iterieren, aber sie bietet eine schöne Abstraktion.
Diese Methode kann als Lua-Iterator aufgerufen werden:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local res, err = cache:get_bulk(bulk)
for i, data, err, hit_lvl in mlcache.each_bulk_res(res) do
if not err then
ngx.say("Lookup ", i, ": ", data)
end
end
peek
syntax: ttl, err, value = cache:peek(key, stale?)
Blickt in den L2 (lua_shared_dict) Cache.
Das erste Argument key ist ein String, der der Schlüssel ist, um im Cache nachzuschlagen.
Das zweite Argument stale ist optional. Wenn true, wird peek() veraltete Werte als zwischengespeicherte Werte betrachten. Wenn nicht angegeben, wird peek() veraltete Werte so betrachten, als wären sie nicht im Cache.
Diese Methode gibt nil und einen String zurück, der den Fehler beschreibt, wenn sie fehlschlägt.
Wenn es keinen Wert für den abgefragten key gibt, gibt sie nil und keinen Fehler zurück.
Wenn es einen Wert für den abgefragten key gibt, gibt sie eine Zahl zurück, die die verbleibende TTL des zwischengespeicherten Wertes (in Sekunden) angibt, und keinen Fehler. Wenn der Wert für key abgelaufen ist, aber noch im L2-Cache vorhanden ist, wird der zurückgegebene TTL-Wert negativ sein. Der verbleibende TTL-Rückgabewert wird nur 0 sein, wenn der abgefragte key eine unbegrenzte TTL hat (ttl=0). Andernfalls kann dieser Rückgabewert positiv (der key ist noch gültig) oder negativ (der key ist veraltet) sein.
Der dritte zurückgegebene Wert ist der zwischengespeicherte Wert, wie er im L2-Cache gespeichert ist, falls noch verfügbar.
Diese Methode ist nützlich, wenn Sie feststellen möchten, ob ein Wert zwischengespeichert ist. Ein Wert, der im L2-Cache gespeichert ist, wird als zwischengespeichert betrachtet, unabhängig davon, ob er auch im L1-Cache des Workers gesetzt ist oder nicht. Das liegt daran, dass der L1-Cache als flüchtig betrachtet wird (da seine Größe in der Anzahl der Slots gemessen wird), und der L2-Cache ist ohnehin mehrere Größenordnungen schneller als der L3-Callback.
Da es nur darum geht, einen "Blick" in den Cache zu werfen, um seine Wärme für einen bestimmten Wert zu bestimmen, zählt peek() nicht als Abfrage wie get() und fördert den Wert nicht in den L1-Cache.
Beispiel:
local mlcache = require "mlcache"
local cache = mlcache.new("my_cache", "cache_shared_dict")
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "konnte Cache nicht einsehen: ", err)
return
end
ngx.say(ttl) -- nil, da `key` noch keinen Wert hat
ngx.say(value) -- nil
-- Wert zwischenspeichern
cache:get("key", { ttl = 5 }, function() return "some value" end)
-- 2 Sekunden warten
ngx.sleep(2)
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "konnte Cache nicht einsehen: ", err)
return
end
ngx.say(ttl) -- 3
ngx.say(value) -- "some value"
Hinweis: Seit mlcache 2.5.0 ist es auch möglich, get() ohne Callback-Funktion aufzurufen, um den Cache zu "abfragen". Im Gegensatz zu peek() wird ein get()-Aufruf ohne Callback den Wert in den L1-Cache befördern und nicht seine TTL zurückgeben.
set
syntax: ok, err = cache:set(key, opts?, value)
Setzt bedingungslos einen Wert im L2-Cache und sendet ein Ereignis an andere Worker, damit sie den Wert aus ihrem L1-Cache aktualisieren können.
Das erste Argument key ist ein String und der Schlüssel, unter dem der Wert gespeichert werden soll.
Das zweite Argument opts ist optional, und wenn angegeben, identisch mit dem von get().
Das dritte Argument value ist der Wert, der zwischengespeichert werden soll, ähnlich dem Rückgabewert des L3-Callbacks. Genau wie der Rückgabewert des Callbacks muss es ein Lua-Skalar, eine Tabelle oder nil sein. Wenn ein l1_serializer bereitgestellt wird, entweder vom Konstruktor oder im opts-Argument, wird es mit value aufgerufen, wenn value nicht nil ist.
Bei Erfolg wird der erste Rückgabewert true sein.
Im Fehlerfall gibt diese Methode nil und einen String zurück, der den Fehler beschreibt.
Hinweis: Aufgrund seiner Natur erfordert set(), dass andere Instanzen von mlcache (von anderen Workern) ihren L1-Cache aktualisieren. Wenn set() von einem einzelnen Worker aufgerufen wird, müssen die mlcache-Instanzen anderer Worker mit dem gleichen name update() aufrufen, bevor ihr Cache bei der nächsten Anfrage angefordert wird, um sicherzustellen, dass sie ihren L1-Cache aktualisiert haben.
Hinweis bis: Es wird allgemein als ineffizient angesehen, set() in einem heißen Codepfad (wie in einer Anfrage, die von OpenResty bedient wird) aufzurufen. Stattdessen sollte man sich auf get() und seinen eingebauten Mutex im L3-Callback verlassen. set() eignet sich besser, wenn es gelegentlich von einem einzelnen Worker aufgerufen wird, z.B. bei einem bestimmten Ereignis, das eine Aktualisierung eines zwischengespeicherten Wertes auslöst. Sobald set() den L2-Cache mit dem frischen Wert aktualisiert, werden andere Worker auf update() angewiesen, um das Invalidierungsereignis abzufragen und ihren L1-Cache ungültig zu machen, was sie dazu bringt, den (frischen) Wert im L2 abzurufen.
Siehe: update()
delete
syntax: ok, err = cache:delete(key)
Löscht einen Wert im L2-Cache und veröffentlicht ein Ereignis an andere Worker, damit sie den Wert aus ihrem L1-Cache entfernen können.
Das erste und einzige Argument key ist der String, unter dem der Wert gespeichert ist.
Bei Erfolg wird der erste Rückgabewert true sein.
Im Fehlerfall gibt diese Methode nil und einen String zurück, der den Fehler beschreibt.
Hinweis: Aufgrund seiner Natur erfordert delete(), dass andere Instanzen von mlcache (von anderen Workern) ihren L1-Cache aktualisieren. Wenn delete() von einem einzelnen Worker aufgerufen wird, müssen die mlcache-Instanzen anderer Worker mit dem gleichen name update() aufrufen, bevor ihr Cache bei der nächsten Anfrage angefordert wird, um sicherzustellen, dass sie ihren L1-Cache aktualisiert haben.
Siehe: update()
purge
syntax: ok, err = cache:purge(flush_expired?)
Bereinigt den Inhalt des Caches auf beiden Ebenen L1 und L2. Dann veröffentlicht es ein Ereignis an andere Worker, damit sie ihren L1-Cache ebenfalls bereinigen können.
Diese Methode recycelt die lua-resty-lrucache-Instanz und ruft ngx.shared.DICT:flush_all auf, sodass sie recht teuer sein kann.
Das erste und einzige Argument flush_expired ist optional, aber wenn true angegeben wird, wird diese Methode auch ngx.shared.DICT:flush_expired (ohne Argumente) aufrufen. Dies ist nützlich, um Speicher freizugeben, der vom L2 (shm) Cache beansprucht wird, falls erforderlich.
Bei Erfolg wird der erste Rückgabewert true sein.
Im Fehlerfall gibt diese Methode nil und einen String zurück, der den Fehler beschreibt.
Hinweis: Es ist nicht möglich, purge() aufzurufen, wenn ein benutzerdefinierter LRU-Cache in OpenResty 1.13.6.1 und darunter verwendet wird. Diese Einschränkung gilt nicht für OpenResty 1.13.6.2 und höher.
Hinweis: Aufgrund seiner Natur erfordert purge(), dass andere Instanzen von mlcache (von anderen Workern) ihren L1-Cache aktualisieren. Wenn purge() von einem einzelnen Worker aufgerufen wird, müssen die mlcache-Instanzen anderer Worker mit dem gleichen name update() aufrufen, bevor ihr Cache bei der nächsten Anfrage angefordert wird, um sicherzustellen, dass sie ihren L1-Cache aktualisiert haben.
Siehe: update()
update
syntax: ok, err = cache:update(timeout?)
Abfragt und führt ausstehende Cache-Invalidierungsereignisse aus, die von anderen Workern veröffentlicht wurden.
Die Methoden set(), delete() und purge() erfordern, dass andere Instanzen von mlcache (von anderen Workern) ihren L1-Cache aktualisieren. Da OpenResty derzeit keinen eingebauten Mechanismus für die Inter-Worker-Kommunikation hat, bündelt dieses Modul eine "fertige" IPC-Bibliothek, um Inter-Worker-Ereignisse zu propagieren. Wenn die gebündelte IPC-Bibliothek verwendet wird, darf der in der ipc_shm-Option angegebene lua_shared_dict nicht von anderen Akteuren als mlcache selbst verwendet werden.
Diese Methode ermöglicht es einem Worker, seinen L1-Cache zu aktualisieren (indem Werte, die aufgrund eines anderen Workers, der set(), delete() oder purge() aufruft, als veraltet betrachtet werden, entfernt werden), bevor eine Anfrage verarbeitet wird.
Diese Methode akzeptiert ein timeout-Argument, dessen Einheit Sekunden ist und standardmäßig auf 0.3 (300 ms) gesetzt ist. Die Aktualisierungsoperation wird zeitlich begrenzt, wenn sie nicht abgeschlossen ist, wenn dieser Schwellenwert erreicht wird. Dies verhindert, dass update() zu lange auf der CPU bleibt, falls es zu viele Ereignisse zu verarbeiten gibt. In einem letztlich konsistenten System können zusätzliche Ereignisse auf den nächsten Aufruf warten, um verarbeitet zu werden.
Ein typisches Entwurfsmuster besteht darin, update() nur einmal vor der Verarbeitung jeder Anfrage aufzurufen. Dies ermöglicht es Ihren heißen Codepfaden, im besten Fall einen einzigen shm-Zugriff durchzuführen: Es wurden keine Invalidierungsereignisse empfangen, alle get()-Aufrufe treffen im L1-Cache. Nur im schlimmsten Fall (wenn n Werte von einem anderen Worker verdrängt wurden) wird get() n-mal auf den L2- oder L3-Cache zugreifen. Nachfolgende Anfragen werden dann erneut den besten Fall treffen, da get() den L1-Cache gefüllt hat.
Wenn Ihre Worker also set(), delete() oder purge() irgendwo in Ihrer Anwendung verwenden, rufen Sie update() am Eingang Ihres heißen Codepfades auf, bevor Sie get() verwenden:
http {
listen 9000;
location / {
content_by_lua_block {
local cache = ... -- mlcache-Instanz abrufen
-- sicherstellen, dass der L1-Cache von veralteten Werten befreit wird
-- bevor get() aufgerufen wird
local ok, err = cache:update()
if not ok then
ngx.log(ngx.ERR, "Fehler beim Abfragen von Invalidierungsereignissen: ", err)
-- /!\ wir könnten veraltete Daten von get() erhalten
end
-- L1/L2/L3 Lookup (bester Fall: L1)
local value, err = cache:get("key_1", nil, cb1)
-- L1/L2/L3 Lookup (bester Fall: L1)
local other_value, err = cache:get("key_2", nil, cb2)
-- value und other_value sind aktuell, weil:
-- entweder waren sie nicht veraltet und kamen direkt aus L1 (bestes Szenario)
-- oder sie waren veraltet und wurden aus L1 verdrängt und kamen aus L2
-- oder sie waren weder in L1 noch in L2 und kamen aus L3 (schlimmstes Szenario)
}
}
location /delete {
content_by_lua_block {
local cache = ... -- mlcache-Instanz abrufen
-- einen Wert löschen
local ok, err = cache:delete("key_1")
if not ok then
ngx.log(ngx.ERR, "Fehler beim Löschen des Wertes aus dem Cache: ", err)
return ngx.exit(500)
end
ngx.exit(204)
}
}
location /set {
content_by_lua_block {
local cache = ... -- mlcache-Instanz abrufen
-- einen Wert aktualisieren
local ok, err = cache:set("key_1", nil, 123)
if not ok then
ngx.log(ngx.ERR, "Fehler beim Setzen des Wertes im Cache: ", err)
return ngx.exit(500)
end
ngx.exit(200)
}
}
}
Hinweis: Sie müssen update() nicht aufrufen, um Ihre Worker zu aktualisieren, wenn sie niemals set(), delete() oder purge() aufrufen. Wenn Worker nur auf get() angewiesen sind, laufen die Werte gemäß ihrer TTL natürlich aus dem L1/L2-Cache ab.
Hinweis bis: Diese Bibliothek wurde mit der Absicht entwickelt, eine bessere Lösung für die Inter-Worker-Kommunikation zu verwenden, sobald eine solche verfügbar ist. In zukünftigen Versionen dieser Bibliothek, wenn eine IPC-Bibliothek den Polling-Ansatz vermeiden kann, wird dies auch diese Bibliothek tun. update() ist nur ein notwendiges Übel aufgrund der heutigen "Einschränkungen" von Nginx/OpenResty. Sie können jedoch Ihre eigene IPC-Bibliothek verwenden, indem Sie die opts.ipc-Option beim Erstellen Ihrer mlcache-Instanz verwenden.
Ressourcen
Im November 2018 wurde diese Bibliothek auf der OpenResty Con in Hangzhou, China, vorgestellt.
Die Folien und eine Aufzeichnung des Vortrags (ca. 40 Minuten lang) können [hier][talk] angesehen werden.
Changelog
Siehe CHANGELOG.md.
GitHub
Sie finden möglicherweise zusätzliche Konfigurationstipps und Dokumentationen für dieses Modul im GitHub-Repository für nginx-module-mlcache.