Top-Tipps: Identifizierung einzigartiger Konzepte und Fallstricke in Lua

API7.ai

October 12, 2022

OpenResty (NGINX + Lua)

Im vorherigen Artikel haben wir die tabellenbezogenen Bibliotheksfunktionen in LuaJIT kennengelernt. Neben diesen gängigen Funktionen werde ich Ihnen heute einige einzigartige oder weniger bekannte Lua-Konzepte sowie häufige Lua-Fallen in OpenResty vorstellen.

Schwache Tabelle

Zunächst gibt es die schwache Tabelle, ein einzigartiges Konzept in Lua, das mit der Garbage Collection zusammenhängt. Wie andere Hochsprachen verfügt Lua über eine automatische Garbage Collection, sodass Sie sich nicht um die Implementierung kümmern müssen und auch keine explizite GC durchführen müssen. Der Garbage Collector sammelt automatisch den Speicherplatz, auf den nicht mehr verwiesen wird.

Aber einfache Referenzzählung reicht nicht immer aus, und manchmal benötigen wir einen flexibleren Mechanismus. Wenn wir beispielsweise ein Lua-Objekt Foo (table oder Funktion) in die Tabelle tb einfügen, wird eine Referenz auf dieses Objekt Foo erstellt. Selbst wenn es keine anderen Referenzen auf Foo gibt, bleibt die Referenz in tb bestehen, sodass der GC den von Foo belegten Speicher nicht freigeben kann. In diesem Fall haben wir nur zwei Möglichkeiten.

  • Die eine besteht darin, Foo manuell freizugeben.
  • Die zweite besteht darin, es dauerhaft im Speicher zu halten.

Beispielsweise der folgende Code.

$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
print(#tb) -- 2

collectgarbage()
print(#tb) -- 2

table.remove(tb, 1)
print(#tb) -- 1

Ich denke jedoch, dass Sie nicht möchten, dass Speicher von Objekten belegt wird, die Sie nicht verwenden, insbesondere da LuaJIT eine Speicherbegrenzung von 2 GB hat. Der Zeitpunkt der manuellen Freigabe ist nicht einfach zu bestimmen und erhöht die Komplexität Ihres Codes.

Dann kommt die schwache Tabelle ins Spiel. Schauen wir uns ihren Namen an, schwache Tabelle. Zunächst ist es eine Tabelle, und dann sind alle Elemente in dieser Tabelle schwache Referenzen. Das Konzept ist immer abstrakt, also schauen wir uns zunächst einen leicht modifizierten Code an.

$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "v"})
print(#tb)  -- 2

collectgarbage()
print(#tb) -- 0
'

Wie Sie sehen, werden nicht verwendete Objekte freigegeben. Der wichtigste Teil davon ist die folgende Codezeile.

setmetatable(tb, {__mode = "v"})

Kommt Ihnen das bekannt vor? Ist das nicht die Operation einer Metatable? Ja, eine Tabelle ist eine schwache Tabelle, wenn sie ein __mode-Feld in ihrer Metatable hat.

  • Wenn der Wert von __mode k ist, ist der Schlüssel der Tabelle eine schwache Referenz.
  • Wenn der Wert von __mode v ist, dann ist der Wert der Tabelle eine schwache Referenz.
  • Natürlich können Sie es auch auf kv setzen, was bedeutet, dass sowohl die Schlüssel als auch die Werte dieser Tabelle schwache Referenzen sind.

Jede dieser drei schwachen Tabellen wird ihre gesamten Schlüssel-Wert-Objekte freigeben, sobald ihr Schlüssel oder Wert freigegeben wird.

Im obigen Codebeispiel ist der Wert von __mode v, tb ist ein Array, und der Wert des Arrays ist die Tabelle und das Funktionsobjekt, sodass es automatisch recycelt werden kann. Wenn Sie jedoch den Wert von __mode auf k ändern, wird es nicht freigegeben, wie im folgenden Code.

$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "k"})
print(#tb)  -- 2

collectgarbage()
print(#tb) -- 2
'

Wir haben nur schwache Tabellen demonstriert, bei denen der Wert eine schwache Referenz ist, d.h. schwache Tabellen vom Array-Typ. Natürlich können Sie auch eine schwache Tabelle vom Hash-Tabellen-Typ erstellen, indem Sie ein Objekt als Schlüssel verwenden, wie im folgenden Beispiel.

$ resty -e 'local tb = {}
tb[{color = red}] = "red"
local fc = function() print("func") end
tb[fc] = "func"
fc = nil

setmetatable(tb, {__mode = "k"})
for k,v in pairs(tb) do
     print(v)
end

collectgarbage()
print("----------")
for k,v in pairs(tb) do
     print(v)
end
'

Nach dem manuellen Aufruf von collectgarbage() zur erzwungenen GC werden alle Elemente in der gesamten Tabelle tb freigegeben. Natürlich müssen wir in tatsächlichem Code collectgarbage() nicht manuell aufrufen, es läuft automatisch im Hintergrund, und wir müssen uns keine Sorgen machen.

Da wir jedoch die Funktion collectgarbage() erwähnt haben, werde ich noch ein paar Worte dazu sagen. Diese Funktion kann mehrere verschiedene Optionen übergeben und standardmäßig collect, was eine vollständige GC ist. Eine weitere nützliche Option ist count, die den von Lua belegten Speicherplatz zurückgibt. Diese Statistik ist hilfreich, um festzustellen, ob ein Speicherleck vorliegt, und erinnert uns daran, die Obergrenze von 2 GB nicht zu überschreiten.

Der Code, der schwache Tabellen betrifft, ist in der Praxis komplizierter zu schreiben, weniger leicht zu verstehen und entsprechend anfälliger für versteckte Fehler. Keine Eile. Später werde ich ein Open-Source-Projekt vorstellen, das durch schwache Tabellen verursachte Speicherlecks behandelt.

Closure und Upvalue

Kommen wir zu Closures und Upvalues. Wie ich bereits betont habe, sind alle Werte in Lua First-Class Citizens, ebenso wie enthaltene Funktionen. Das bedeutet, dass Funktionen in Variablen gespeichert, als Argumente übergeben und als Werte einer anderen Funktion zurückgegeben werden können. Beispielsweise erscheint dieser Beispielcode in der schwachen Tabelle oben.

tb[2] = function() print("func") end

Es handelt sich um eine anonyme Funktion, die als Wert einer Tabelle gespeichert ist.

In Lua ist die Definition der beiden Funktionen im folgenden Code äquivalent. Beachten Sie jedoch, dass letztere einer Variablen eine Funktion zuweist, eine Methode, die wir häufig verwenden.

local function foo() print("foo") end
local foo = fuction() print("foo") end

Darüber hinaus unterstützt Lua das Schreiben einer Funktion innerhalb einer anderen Funktion, d.h. verschachtelte Funktionen, wie im folgenden Beispielcode.

$ resty -e '
local function foo()
     local i = 1
     local function bar()
         i = i + 1
         print(i)
     end
     return bar
end

local fn = foo()
print(fn()) -- 2
'

Sie können sehen, dass die Funktion bar die lokale Variable i innerhalb der Funktion foo lesen und ihren Wert ändern kann, auch wenn die Variable nicht innerhalb von bar definiert ist. Diese Funktion wird als lexikalische Gültigkeitsbereich bezeichnet.

Diese Funktionen von Lua sind die Grundlage für Closures. Ein Closure ist einfach eine Funktion, die auf eine Variable im lexikalischen Gültigkeitsbereich einer anderen Funktion zugreift.

Per Definition sind alle Funktionen in Lua tatsächlich Closures, auch wenn Sie sie nicht verschachteln. Dies liegt daran, dass der Lua-Compiler außerhalb des Lua-Skripts liegt und es mit einer weiteren Schicht der Hauptfunktion umhüllt. Beispielsweise die folgenden einfachen Codezeilen.

local foo, bar
local function fn()
     foo = 1
     bar = 2
end

Nach der Kompilierung sieht es so aus.

function main(...)
     local foo, bar
     local function fn()
         foo = 1
         bar = 2
     end
end

Und die Funktion fn erfasst zwei lokale Variablen der Hauptfunktion, daher ist sie auch ein Closure.

Natürlich wissen wir, dass das Konzept der Closures in vielen Sprachen existiert und nicht einzigartig für Lua ist, sodass Sie vergleichen und gegenüberstellen können, um ein besseres Verständnis zu erlangen. Nur wenn Sie Closures verstehen, können Sie verstehen, was wir über upvalue sagen werden.

upvalue ist ein Konzept, das einzigartig für Lua ist, nämlich die Variable außerhalb des lexikalischen Gültigkeitsbereichs, die im Closure erfasst wird. Fahren wir mit dem obigen Code fort.

local foo, bar
local function fn()
     foo = 1
     bar = 2
end

Sie können sehen, dass die Funktion fn zwei lokale Variablen, foo und bar, erfasst, die sich nicht in ihrem eigenen lexikalischen Gültigkeitsbereich befinden, und dass diese beiden Variablen tatsächlich die upvalue der Funktion fn sind.

Häufige Fallen

Nach der Einführung einiger Konzepte in Lua werde ich über die Lua-bezogenen Fallen sprechen, auf die ich bei der Entwicklung von OpenResty gestoßen bin.

Im vorherigen Abschnitt haben wir einige der Unterschiede zwischen Lua und anderen Entwicklungssprachen erwähnt, wie z.B. dass der Index bei 1 beginnt, standardmäßig globale Variablen usw. In der tatsächlichen Code-Entwicklung von OpenResty werden wir auf mehr Lua- und LuaJIT-bezogene Probleme stoßen, und ich werde im Folgenden einige der häufigeren besprechen.

Hier eine Erinnerung: Selbst wenn Sie alle Fallen kennen, werden Sie unweigerlich selbst durch sie hindurchgehen müssen, um einen bleibenden Eindruck zu hinterlassen. Der Unterschied besteht natürlich darin, dass Sie in der Lage sein werden, aus der Falle herauszuklettern und den Kern des Problems viel besser zu finden.

Beginnt der Index bei 0 oder 1?

Die erste Falle ist, dass der Index in Lua bei 1 beginnt, wie wir bereits mehrfach erwähnt haben.

Aber ich muss sagen, dass dies nicht die ganze Wahrheit ist. Denn in LuaJIT beginnen Arrays, die mit ffi.new erstellt wurden, wieder bei 0:

local buf = ffi_new("char[?]", 128)

Wenn Sie also auf das buf-cdata im obigen Code zugreifen möchten, denken Sie daran, dass der Index bei 0 beginnt, nicht bei 1. Achten Sie besonders darauf, wenn Sie FFI zur Interaktion mit C verwenden.

Regulärer Ausdruck-Matching

Die zweite Falle ist das Problem des regulären Ausdruck-Matchings, und in OpenResty gibt es zwei parallele Sätze von String-Matching-Methoden: die Lua-Sting-Bibliothek und die OpenResty-ngx.re.*-API.

Das reguläre Ausdruck-Matching von Lua hat sein eigenes Format und wird anders geschrieben als PCRE. Hier ist ein einfaches Beispiel.

resty -e 'print(string.match("foo 123 bar", "%d%d%d"))'123

Dieser Code extrahiert den numerischen Teil aus dem String, und Sie werden feststellen, dass er sich völlig von unseren vertrauten regulären Ausdrücken unterscheidet. Die reguläre Matching-Bibliothek von Lua ist teuer in der Wartung und leistungsschwach - JIT kann sie nicht optimieren, und einmal kompilierte Muster werden nicht zwischengespeichert.

Wenn Sie also die eingebaute String-Bibliothek von Lua verwenden, um find, match usw. durchzuführen, zögern Sie nicht, stattdessen OpenResty's ngx.re zu verwenden, wenn Sie etwas wie einen regulären Ausdruck benötigen. Wenn Sie nach einem festen String suchen, sollten Sie den einfachen Modus verwenden, um die String-Bibliothek aufzurufen.

Hier ein Vorschlag: In OpenResty priorisieren wir immer die OpenResty-API, dann die LuaJIT-API und verwenden Lua-Bibliotheken mit Vorsicht.

JSON-Kodierung unterscheidet nicht zwischen Array und Dict

Die dritte Falle ist, dass die JSON-Kodierung nicht zwischen Array und Dict unterscheidet; da Lua nur eine Datenstruktur hat, table, gibt es keine Möglichkeit, festzustellen, ob es sich um ein Array oder ein Wörterbuch handelt, wenn JSON eine leere Tabelle kodiert.

resty -e 'local cjson = require "cjson"
local t = {}
print(cjson.encode(t))
'

Beispielsweise gibt der obige Code {} aus, was zeigt, dass die cjson-Bibliothek von OpenResty eine leere Tabelle standardmäßig als Wörterbuch kodiert. Natürlich können wir diese globale Standardeinstellung mit der Funktion encode_empty_table_as_object ändern.

resty -e 'local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))
'

Diesmal wird die leere Tabelle als Array [] kodiert.

Diese globale Einstellung hat jedoch erhebliche Auswirkungen. Können wir also die Kodierungsregeln für eine bestimmte Tabelle festlegen? Die Antwort ist natürlich ja, und es gibt zwei Möglichkeiten.

Die erste Möglichkeit besteht darin, das userdata cjson.empty_array der angegebenen Tabelle zuzuweisen, sodass es bei der JSON-Kodierung als leeres Array behandelt wird.

$ resty -e 'local cjson = require "cjson"
local t = cjson.empty_array
print(cjson.encode(t))
'

Manchmal sind wir uns jedoch nicht sicher, ob die angegebene Tabelle immer leer ist. Wir möchten sie als Array kodieren, wenn sie leer ist, also verwenden wir die Funktion cjson.empty_array_mt, was unsere zweite Methode ist.

Sie markiert die angegebene Tabelle und kodiert sie als Array, wenn die Tabelle leer ist. Wie Sie am Namen cjson.empty_array_mt erkennen können, wird sie mit einer metatable festgelegt, wie im folgenden Code.

$ resty -e 'local cjson = require "cjson"
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))
'

Begrenzung der Anzahl der Variablen

Schauen wir uns die vierte Falle an, die Begrenzung der Anzahl der Variablen. Lua hat eine Obergrenze für die Anzahl der lokalen Variablen und die Anzahl der upvalues in einer Funktion, wie Sie aus dem Lua-Quellcode ersehen können.

/*
@@ LUAI_MAXVARS ist die maximale Anzahl lokaler Variablen pro Funktion
@* (muss kleiner als 250 sein).
*/
#define LUAI_MAXVARS            200


/*
@@ LUAI_MAXUPVALUES ist die maximale Anzahl von Upvalues pro Funktion
@* (muss kleiner als 250 sein).
*/
#define LUAI_MAXUPVALUES        60

Diese beiden Schwellenwerte sind hartkodiert auf 200 bzw. 60, und obwohl Sie den Quellcode manuell ändern können, um diese Werte anzupassen, können sie nur auf maximal 250 gesetzt werden.

Im Allgemeinen überschreiten wir diesen Schwellenwert nicht. Dennoch sollten Sie beim Schreiben von OpenResty-Code darauf achten, nicht zu viele lokale Variablen und upvalues zu verwenden, sondern so weit wie möglich do ... end zu verwenden, um die Anzahl der lokalen Variablen und upvalues zu reduzieren.

Schauen wir uns beispielsweise den folgenden Pseudocode an.

local re_find = ngx.re.find
function foo() ... end
function bar() ... end
function fn() ... end

Wenn nur die Funktion foo re_find verwendet, können wir sie wie folgt ändern:

do
    local re_find = ngx.re.find
    function foo() ... end
end
function bar() ... end
function fn() ... end

Zusammenfassung

Aus der Sicht von "mehr Fragen stellen", woher kommt der Schwellenwert von 250 in Lua? Dies ist unsere heutige Denkfrage. Sie sind eingeladen, Ihre Kommentare zu hinterlassen und diesen Artikel mit Ihren Kollegen und Freunden zu teilen. Wir werden gemeinsam kommunizieren und uns verbessern.

Share article link