Why Does lua-resty-core Perform Better?
API7.ai
September 30, 2022
Wie wir in den vorherigen beiden Lektionen gesagt haben, ist Lua eine eingebettete Entwicklungssprache, die den Kern kurz und kompakt hält. Sie können Lua in Redis und NGINX einbetten, um Ihnen zu helfen, Geschäftslogik flexibel zu gestalten. Lua ermöglicht es Ihnen auch, bestehende C-Funktionen und Datenstrukturen aufzurufen, um das Rad nicht neu zu erfinden.
In Lua können Sie die Lua C API verwenden, um C-Funktionen aufzurufen, und in LuaJIT können Sie FFI verwenden. Für OpenResty.
- Im Kern
lua-nginx-module
wird die API für den Aufruf von C-Funktionen mit der Lua C API durchgeführt. - In
lua-resty-core
sind einige der APIs, die bereits imlua-nginx-module
enthalten sind, mit dem FFI-Modell implementiert.
Sie fragen sich wahrscheinlich, warum wir es mit FFI implementieren müssen?
Machen Sie sich keine Sorgen. Nehmen wir ngx.base64_decode, eine einfache API, als Beispiel und sehen wir, wie sich die Lua C API von der FFI-Implementierung unterscheidet. Sie können auch ein intuitives Verständnis für ihre Leistung gewinnen.
Lua CFunction
Schauen wir uns an, wie dies im lua-nginx-module
mit der Lua C API implementiert ist. Wir suchen nach decode_base64
im Code des Projekts und finden seine Implementierung in ngx_http_lua_string.c
.
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64);
lua_setfield(L, -2, "decode_base64");
Der obige Code ist ärgerlich anzusehen, aber zum Glück müssen wir uns nicht in die beiden Funktionen, die mit lua_
beginnen, und die spezifische Rolle ihrer Argumente vertiefen; wir müssen nur eines wissen - hier ist eine CFunction registriert: ngx_http_lua_ngx_decode_base64
, und sie entspricht der ngx.base64_decode
, die der API entspricht, die öffentlich zugänglich gemacht wird.
Lassen Sie uns weiter "der Karte folgen" und nach ngx_http_lua_ngx_decode_base64
in dieser C-Datei suchen, die am Anfang der Datei definiert ist:
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
Für die C-Funktionen, die von Lua aufgerufen werden können, muss ihre Schnittstelle die von Lua erforderte Form einhalten, nämlich typedef int (*lua_CFunction)(lua_State* L)
. Sie enthält einen Zeiger L vom Typ lua_State
als Argument; ihr Rückgabewerttyp ist eine Ganzzahl, die die Anzahl der zurückgegebenen Werte angibt, nicht den Rückgabewert selbst.
Sie ist wie folgt implementiert (hier habe ich den Fehlerbehandlungscode entfernt).
static int
ngx_http_lua_ngx_decode_base64(lua_State *L)
{
ngx_str_t p, src;
src.data = (u_char *) luaL_checklstring(L, 1, &src.len);
p.len = ngx_base64_decoded_length(src.len);
p.data = lua_newuserdata(L, p.len);
if (ngx_decode_base64(&p, &src) == NGX_OK) {
lua_pushlstring(L, (char *) p.data, p.len);
} else {
lua_pushnil(L);
}
return 1;
}
Das Wichtigste in diesem Code sind ngx_base64_decoded_length
und ngx_decode_base64
, beides C-Funktionen, die von NGINX bereitgestellt werden.
Wir wissen, dass Funktionen, die in C geschrieben sind, den Rückgabewert nicht an Lua-Code übergeben können, sondern die Aufrufparameter und den Rückgabewert zwischen Lua und C über den Stack übergeben müssen. Deshalb gibt es viel Code, den wir auf den ersten Blick nicht verstehen können. Außerdem kann dieser Code nicht von JIT verfolgt werden, daher sind diese Operationen für LuaJIT in einer Blackbox und können nicht optimiert werden.
LuaJIT FFI
Im Gegensatz zu FFI wird der interaktive Teil von FFI in Lua implementiert, was von JIT verfolgt und optimiert werden kann; natürlich ist der Code auch prägnanter und leichter zu verstehen.
Nehmen wir das Beispiel base64_decode
, dessen FFI-Implementierung über zwei Repositories verteilt ist: lua-resty-core
und lua-nginx-module
, und schauen wir uns den implementierten Code im ersteren an.
ngx.decode_base64 = function (s)
local slen = #s
local dlen = base64_decoded_length(slen)
local dst = get_string_buf(dlen)
local pdlen = get_size_ptr()
local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen)
if ok == 0 then
return nil
end
return ffi_string(dst, pdlen[0])
end
Sie werden feststellen, dass der Code der FFI-Implementierung im Vergleich zu CFunction viel frischer ist, seine spezifische Implementierung ist ngx_http_lua_ffi_decode_base64
im lua-nginx-module
-Repository. Wenn Sie hier interessiert sind, können Sie die Leistung dieser Funktion selbst überprüfen. Es ist sehr einfach, ich werde den Code hier nicht posten.
Aber wenn Sie aufmerksam sind, haben Sie einige Funktionsbenennungsregeln im obigen Codeausschnitt gefunden?
Ja, alle Funktionen in OpenResty haben Benennungskonventionen, und Sie können ihre Verwendung durch ihre Benennung ableiten. Zum Beispiel:
ngx_http_lua_ffi_
, die Lua-Funktion, die FFI verwendet, um NGINX HTTP-Anfragen zu verarbeiten.ngx_http_lua_ngx_
, eine Lua-Funktion, die C-Funktionen verwendet, um NGINX HTTP-Anfragen zu verarbeiten.- Die anderen Funktionen, die mit ngx und lua beginnen, sind eingebaute Funktionen für NGINX bzw. Lua.
Darüber hinaus hat der C-Code in OpenResty eine strenge Code-Spezifikation, und ich empfehle, den offiziellen C-Code Stil-Leitfaden hier zu lesen. Dies ist ein Muss für Entwickler, die OpenResty's C-Code lernen und PRs einreichen möchten. Andernfalls wird Ihr PR, auch wenn er gut geschrieben ist, aufgrund von Code-Stilproblemen wiederholt kommentiert und Sie werden aufgefordert, ihn zu ändern.
Für weitere API und Details über FFI empfehlen wir Ihnen, die offiziellen LuaJIT-Tutorials und Dokumentation zu lesen. Technische Kolumnen sind kein Ersatz für offizielle Dokumentation; ich kann Ihnen nur helfen, den Lernpfad in begrenzter Zeit mit weniger Umwegen aufzuzeigen; schwierige Probleme müssen immer noch von Ihnen gelöst werden.
LuaJIT FFI GC
Bei der Verwendung von FFI können wir verwirrt sein: Wer verwaltet den in FFI angeforderten Speicher? Sollten wir ihn manuell in C freigeben, oder sollte LuaJIT ihn automatisch zurückfordern?
Hier ist ein einfaches Prinzip: LuaJIT ist nur für die Ressourcen verantwortlich, die es selbst zuweist; ffi.
Zum Beispiel, wenn Sie einen Speicherblock mit ffi.C.malloc
anfordern, müssen Sie ihn mit dem gepaarten ffi.C.free
freigeben. Die offizielle LuaJIT-Dokumentation hat ein Beispiel dafür.
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
...
p = nil -- Letzte Referenz auf p ist weg.
-- GC wird schließlich den Finalizer ausführen: ffi.C.free(p)
In diesem Code fordert ffi.C.malloc(n)
einen Speicherabschnitt an, und ffi.gc
registriert eine Destruktor-Callback-Funktion ffi.C.free
,ffi.C.free
wird dann automatisch aufgerufen, wenn ein cdata p
von LuaJIT GC'd wird, um den C-Level-Speicher freizugeben. Und cdata wird von LuaJIT GC'd. LuaJIT wird p
im obigen Code automatisch freigeben.
Beachten Sie, dass wenn Sie einen großen Speicherblock in OpenResty anfordern möchten, ich empfehle, ffi.C.malloc anstelle von ffi.new zu verwenden. Die Gründe sind auch offensichtlich.
ffi.new
gibtcdata
zurück, das Teil des von LuaJIT verwalteten Speichers ist.- LuaJIT GC hat eine Obergrenze für die Speicherverwaltung, und LuaJIT in OpenResty hat die GC64-Option nicht aktiviert. Daher beträgt die Obergrenze des Speichers für einen einzelnen Worker nur 2G. Sobald die Obergrenze der LuaJIT-Speicherverwaltung überschritten wird, führt dies zu einem Fehler.
Bei der Verwendung von FFI müssen wir auch besonders auf Speicherlecks achten. Aber jeder macht Fehler, und solange Menschen den Code schreiben, gibt es immer Bugs.
Hier kommt die robuste Umgebungstest- und Debugging-Toolchain von OpenResty ins Spiel.
Lassen Sie uns zuerst über Tests sprechen. Im OpenResty-System verwenden wir Valgrind, um Speicherlecks zu erkennen.
Das Testframework, das wir im vorherigen Kurs erwähnt haben, test::nginx
, hat einen speziellen Speicherleck-Erkennungsmodus, um Unit-Test-Fälle auszuführen; Sie müssen die Umgebungsvariable TEST_NGINX_USE_VALGRIND=1
setzen. Das offizielle OpenResty-Projekt wird vor der Veröffentlichung der Version vollständig in diesem Modus registriert, und wir werden später im Testabschnitt mehr ins Detail gehen. Wir werden später im Testabschnitt mehr ins Detail gehen.
OpenResty's CLI resty hat auch die --valgrind
-Option, mit der Sie einen Lua-Code alleine ausführen können, auch wenn Sie keinen Testfall geschrieben haben.
Schauen wir uns die Debugging-Tools an.
OpenResty bietet systemtap-basierte Erweiterungen an, um eine Live-Dynamikanalyse von OpenResty-Programmen durchzuführen. Sie können nach dem Schlüsselwort gc
im Toolset dieses Projekts suchen, und Sie werden zwei Tools sehen, lj-gc
und lj-gc-objs
.
Für Offline-Analysen wie core dump
bietet OpenResty ein GDB-Toolset, und Sie können auch nach gc
darin suchen und die drei Tools lgc
, lgcstat
und lgcpath
finden.
Die spezifische Verwendung dieser Debugging-Tools wird später im Debugging-Abschnitt ausführlich behandelt, damit Sie einen Eindruck bekommen können. Schließlich hat OpenResty ein eigenes Set von Tools, um Ihnen zu helfen, diese Probleme zu lokalisieren und zu lösen.
lua-resty-core
Aus dem obigen Vergleich können wir sehen, dass der FFI-Ansatz nicht nur sauberer im Code ist, sondern auch von LuaJIT optimiert werden kann, was die bessere Wahl ist. OpenResty hat die CFunction-Implementierung veraltet, und die Leistung wurde aus dem Codebase entfernt. Die neuen APIs werden jetzt im lua-resty-core
-Repository über FFI implementiert.
Bevor OpenResty's 1.15.8.1 im Mai 2019 veröffentlicht wurde, war lua-resty-core
nicht standardmäßig aktiviert, was zu Leistungsverlusten und potenziellen Bugs führte, daher empfehle ich dringend, dass jeder, der noch die historische Version verwendet, lua-resty-core
manuell aktiviert. Sie müssen nur eine Codezeile in der init_by_lua
-Phase hinzufügen.
require "resty.core"
Natürlich wurde die lua_load_resty_core-Direktive in der verspäteten 1.15.8.1-Version hinzugefügt, und lua-resty-core
ist standardmäßig aktiviert.
Ich persönlich finde, dass OpenResty immer noch zu vorsichtig ist, wenn es darum geht, lua-resty-core
zu aktivieren, und Open-Source-Projekte sollten ähnliche Funktionen so schnell wie möglich standardmäßig aktivieren.
lua-resty-core
implementiert nicht nur einige der APIs aus dem lua-nginx-module
-Projekt neu, wie ngx.re.match
, ngx.md5
usw., sondern implementiert auch mehrere neue APIs, wie ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.process
, und ngx.ngx.md5
. ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
usw., die wir später im OpenResty API-Kapitel behandeln werden.
Zusammenfassung
Nach all dem möchte ich zusammenfassen, dass FFI, obwohl gut, kein Leistungssilbergeschoss ist. Der Hauptgrund, warum es effizient ist, ist, dass es von JIT verfolgt und optimiert werden kann. Wenn Sie Lua-Code schreiben, der nicht JIT'd werden kann und im Interpretationsmodus ausgeführt werden muss, dann wird FFI weniger effizient sein.
Also, welche Operationen können JIT'd werden und welche nicht? Wie können wir vermeiden, Code zu schreiben, der nicht JIT'd werden kann? Ich werde dies im nächsten Abschnitt enthüllen.
Schließlich eine praktische Hausaufgabe: Können Sie eine oder zwei APIs sowohl in lua-nginx-module
als auch in lua-resty-core
finden und dann die Unterschiede in Leistungstests vergleichen? Sie können sehen, wie signifikant die Leistungsverbesserung von FFI ist.
Willkommen, einen Kommentar zu hinterlassen, und ich werde Ihre Gedanken und Erkenntnisse teilen und Sie einladen, diesen Artikel mit Ihren Kollegen und Freunden zu teilen, um gemeinsam auszutauschen und Fortschritte zu machen.