OpenResty FAQ | Dynamisches Laden, NYI und Caching von Shared Dict
API7.ai
January 19, 2023
Die Openresty-Artikelserie wurde bisher aktualisiert, und der Teil über Leistungsoptimierung ist alles, was wir gelernt haben. Glückwunsch an Sie, dass Sie nicht zurückgeblieben sind, sondern weiterhin aktiv lernen und praktizieren und begeistert Ihre Gedanken hinterlassen.
Wir haben viele typische und interessante Fragen gesammelt, und hier ist ein Blick auf fünf davon.
Frage 1: Wie erreiche ich das dynamische Laden von Lua-Modulen?
Beschreibung: Ich habe eine Frage zum dynamischen Laden, das in OpenResty implementiert ist. Wie kann ich die Funktion
loadstring
verwenden, um das Laden einer neuen Datei abzuschließen, nachdem sie ersetzt wurde? Ich verstehe, dassloadstring
nur Strings laden kann. Wenn ich also eine Lua-Datei/ein Lua-Modul neu laden möchte, wie kann ich das in OpenResty tun?
Wie wir wissen, wird loadstring
verwendet, um einen String zu laden, während loadfile
eine bestimmte Datei laden kann, zum Beispiel: loadfile("foo.lua")
. Diese beiden Befehle erreichen das gleiche Ergebnis. Was das Laden von Lua-Modulen betrifft, hier ist ein Beispiel:
resty -e 'local s = [[
local ngx = ngx
local _M = {}
function _M.f()
ngx.say("hello world")
end
return _M
]]
local lua = loadstring(s)
local ret, func = pcall(lua)
func.f()'
Der Inhalt des Strings s
ist ein vollständiges Lua-Modul. Wenn Sie also eine Änderung im Code dieses Moduls feststellen, können Sie das Laden mit loadstring
oder loadfile
neu starten. Auf diese Weise werden die Funktionen und Variablen darin aktualisiert.
Um einen Schritt weiter zu gehen, können Sie das Abrufen von Änderungen und das Neuladen auch mit einer Funktion namens code_loader
umschließen.
local func = code_loader(name)
Dies macht Code-Updates viel übersichtlicher. Gleichzeitig verwendet code_loader
in der Regel lru cache
, um s
zu cachen, um zu vermeiden, dass jedes Mal loadstring
aufgerufen wird.
Frage 2: Warum verbietet OpenResty blockierende Operationen nicht?
Beschreibung: Über die Jahre habe ich mich immer gefragt, warum diese blockierenden Aufrufe, die offiziell nicht empfohlen werden, nicht einfach deaktiviert werden? Oder warum nicht ein Flag hinzufügen, das dem Benutzer die Wahl lässt, sie zu deaktivieren?
Hier ist meine persönliche Meinung. Erstens, weil das Ökosystem um OpenResty nicht perfekt ist, müssen wir manchmal blockierende Bibliotheken aufrufen, um einige Funktionen zu implementieren. Zum Beispiel mussten Sie vor Version 1.15.8 die Lua-Bibliothek os.execute
anstelle von lua-resty-shell
verwenden, um externe Befehle aufzurufen. Zum Beispiel ist das Lesen und Schreiben von Dateien in OpenResty immer noch nur mit der Lua-I/O-Bibliothek möglich, und es gibt keine nicht-blockierende Alternative.
Zweitens ist OpenResty bei solchen Optimierungen sehr vorsichtig. Zum Beispiel wurde lua-resty-core
lange entwickelt, aber es wurde nie standardmäßig aktiviert, sondern Sie mussten manuell require 'resty.core'
aufrufen. Es wurde erst mit der neuesten Version 1.15.8 standardmäßig aktiviert.
Schließlich bevorzugen die OpenResty-Maintainer, blockierende Aufrufe zu standardisieren, indem sie hochoptimierten Lua-Code durch den Compiler und DSL automatisch generieren. Daher gibt es keine Bemühungen, etwas wie Flag-Optionen auf der OpenResty-Plattform selbst zu implementieren. Natürlich bin ich unsicher, ob diese Richtung das Problem lösen kann.
Aus der Sicht eines externen Entwicklers ist das praktischere Problem, wie man solche Blockierungen vermeiden kann. Wir können Lua-Code-Überprüfungstools wie luacheck
erweitern, um häufige blockierende Operationen zu finden und zu melden, oder wir können bestimmte Funktionen direkt durch das Überschreiben von _G
deaktivieren oder umschreiben, zum Beispiel:
resty -e '_G.ngx.print = function()
ngx.say("hello")
end
ngx.print()'
# hello
Mit diesem Beispielcode können Sie die Funktion ngx.print
direkt umschreiben.
Frage 3: Hat die Operation von LuaJITs NYI einen signifikanten Einfluss auf die Leistung?
Beschreibung:
loadstring
zeigtnever
in LuaJITs NYI-Liste an. Wird es einen großen Einfluss auf die Leistung haben?
Was LuaJITs NYI betrifft, müssen wir nicht zu streng sein. Für Operationen, die JIT-fähig sind, ist der JIT-Ansatz natürlich der beste; aber für Operationen, die noch nicht JIT-fähig sind, können wir sie weiterhin verwenden.
Für die Leistungsoptimierung müssen wir einen statistisch basierten wissenschaftlichen Ansatz verfolgen, was die Flammengrafik-Erfassung ausmacht. Vorzeitige Optimierung ist die Wurzel allen Übels. Wir müssen nur Hot-Code optimieren, der viele Aufrufe macht und viel CPU verbraucht.
Zurück zu loadstring
, wir werden es nur aufrufen, um neu zu laden, wenn sich der Code ändert, nicht bei jeder Anfrage, daher ist es keine häufige Operation. An diesem Punkt müssen wir uns keine Sorgen über seinen Einfluss auf die Gesamtleistung des Systems machen.
In Verbindung mit dem zweiten Blockierungsproblem rufen wir in OpenResty manchmal auch blockierende Datei-I/O-Operationen während der init
- und init worker
-Phasen auf. Diese Operation ist leistungsmindernder als NYI, aber da sie nur einmal beim Start des Dienstes durchgeführt wird, ist sie akzeptabel.
Wie immer sollte die Leistungsoptimierung aus einer makroskopischen Perspektive betrachtet werden, ein Punkt, auf den Sie besonders achten müssen. Andernfalls werden Sie wahrscheinlich lange optimieren, aber keinen guten Effekt erzielen.
Frage 4: Kann ich dynamisches Upstream selbst implementieren?
Beschreibung: Für dynamisches Upstream ist mein Ansatz, 2 Upstreams für einen Dienst einzurichten, je nach Routing-Bedingungen unterschiedliche Upstreams auszuwählen und die IP im Upstream direkt zu ändern, wenn sich die Maschinen-IP ändert. Gibt es Nachteile oder Fallstricke bei diesem Ansatz im Vergleich zur direkten Verwendung von
balancer_by_lua
?
Der Vorteil von balancer_by_lua
ist, dass der Benutzer den Lastausgleichsalgorithmus wählen kann, zum Beispiel ob roundrobin
oder chash
verwendet wird, oder einen anderen Algorithmus, den der Benutzer implementiert, was flexibel und leistungsstark ist.
Wenn Sie es auf die Weise von Routing-Regeln tun, ist das Ergebnis das gleiche. Aber die Upstream-Gesundheitsüberprüfung muss von Ihnen implementiert werden, was viel zusätzliche Arbeit bedeutet.
Wir können diese Frage auch erweitern, indem wir fragen, wie wir dieses Szenario für abtest
implementieren sollten, was einen anderen Upstream erfordert.
Sie können in der balancer_by_lua
-Phase basierend auf uri
, host
, Parametern
usw. entscheiden, welchen Upstream Sie verwenden möchten. Sie können auch API-Gateways verwenden, um diese Entscheidungen in Routing-Regeln umzuwandeln, die in der anfänglichen access
-Phase entscheiden, welche Route verwendet wird, und dann den angegebenen Upstream durch die Bindungsbeziehung zwischen Route und Upstream finden. Dies ist ein gängiger Ansatz bei API-Gateways, und wir werden später im praktischen Teil ausführlicher darüber sprechen.
Frage 5: Ist das Caching von shared dict
obligatorisch?
Beschreibung:
In realen Produktionsanwendungen denke ich, dass die Cache-Ebene des
shared dict
ein Muss ist. Es scheint, dass sich jeder nur an die Vorteile vonlru cache
erinnert, keine Einschränkungen beim Datenformat, keine Notwendigkeit zur Deserialisierung, keine Berechnung des Speicherplatzes basierend auf dem k/v-Volumen, keine Konflikte zwischen Workern, keine Lese-/Schreibsperren und hohe Leistung.Aber ignorieren Sie nicht, dass einer seiner tödlichsten Schwachpunkte darin besteht, dass der Lebenszyklus des
lru cache
demWorker
folgt. Immer wenn NGINX neu geladen wird, geht dieser Teil des Caches vollständig verloren, und zu diesem Zeitpunkt, wenn es keinenshared dict
gibt, wird dieL3
-Datenquelle in Minuten überlastet sein.Natürlich ist dies der Fall bei höherer Parallelität, aber da Caching verwendet wird, ist das Geschäftsvolumen sicherlich nicht klein, was bedeutet, dass die gerade erwähnte Analyse immer noch zutrifft. Wenn ich in dieser Ansicht richtig liege?
In einigen Fällen ist es tatsächlich so, wie Sie sagten, dass der shared dict
beim Neuladen nicht verloren geht, daher ist er notwendig. Aber es gibt einen besonderen Fall, in dem nur der lru cache
akzeptabel ist, wenn alle Daten aktiv von der L3
-Datenquelle in der init
-Phase oder init_worker
-Phase verfügbar sind.
Zum Beispiel, wenn das Open-Source-API-Gateway APISIX seine Datenquelle in etcd
hat, holt es nur Daten von etcd
. Es cached sie im lru cache
während der init_worker
-Phase, und spätere Cache-Updates werden aktiv durch den watch
-Mechanismus von etcd
abgerufen. Auf diese Weise gibt es selbst bei einem NGINX-Reload keinen Cache-Stampede.
Daher können wir bei der Wahl der Technologie Vorlieben haben, aber nicht absolut verallgemeinern, weil es keine Universallösung gibt, die alle Caching-Szenarien abdeckt. Es ist ein ausgezeichneter Weg, eine minimal verfügbare Lösung gemäß den Anforderungen des tatsächlichen Szenarios zu erstellen und sie dann schrittweise zu erweitern.