Pourquoi lua-resty-core offre-t-il de meilleures performances ?
API7.ai
September 30, 2022
Comme nous l'avons dit dans les deux leçons précédentes, Lua est un langage de développement embarqué qui maintient le noyau court et compact. Vous pouvez intégrer Lua dans Redis et NGINX pour vous aider à implémenter de manière flexible la logique métier. Lua vous permet également d'appeler des fonctions et des structures de données C existantes pour éviter de réinventer la roue.
Dans Lua, vous pouvez utiliser l'API Lua C pour appeler des fonctions C, et dans LuaJIT, vous pouvez utiliser FFI. Pour OpenResty :
- Dans le module central
lua-nginx-module
, l'API pour appeler des fonctions C est réalisée en utilisant l'API Lua C. - Dans
lua-resty-core
, certaines des API déjà présentes danslua-nginx-module
sont implémentées en utilisant le modèle FFI.
Vous vous demandez probablement pourquoi nous avons besoin de l'implémenter avec FFI ?
Ne vous inquiétez pas. Prenons ngx.base64_decode, une API simple, comme exemple et voyons comment l'API Lua C diffère de l'implémentation FFI. Vous pourrez ainsi avoir une compréhension intuitive de leurs performances.
Lua CFunction
Voyons comment cela est implémenté dans lua-nginx-module
en utilisant l'API Lua C. Nous recherchons decode_base64
dans le code du projet et trouvons son implémentation dans ngx_http_lua_string.c
.
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64);
lua_setfield(L, -2, "decode_base64");
Le code ci-dessus est ennuyeux à regarder, mais heureusement, nous n'avons pas besoin de creuser dans les deux fonctions commençant par lua_
et le rôle spécifique de leurs arguments ; nous avons juste besoin de savoir une chose - il y a une CFunction enregistrée ici : ngx_http_lua_ngx_decode_base64
, et elle correspond à ngx.base64_decode
, qui correspond à l'API exposée au public.
Continuons et "suivons la carte" en recherchant ngx_http_lua_ngx_decode_base64
dans ce fichier C, qui est défini au début du fichier à :
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
Pour les fonctions C qui peuvent être appelées par Lua, leur interface doit suivre la forme requise par Lua, qui est typedef int (*lua_CFunction)(lua_State* L)
. Elle contient un pointeur L de type lua_State
comme argument ; son type de retour est un entier qui indique le nombre de valeurs retournées, et non la valeur de retour elle-même.
Elle est implémentée comme suit (ici, j'ai supprimé le code de gestion des erreurs).
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;
}
L'essentiel de ce code est ngx_base64_decoded_length
, et ngx_decode_base64
, qui sont toutes deux des fonctions C fournies par NGINX.
Nous savons que les fonctions écrites en C ne peuvent pas passer la valeur de retour au code Lua mais doivent passer les paramètres d'appel et retourner une valeur entre Lua et C via la pile. C'est pourquoi il y a beaucoup de code que nous ne comprenons pas au premier coup d'œil. De plus, ce code ne peut pas être suivi par JIT, donc pour LuaJIT, ces opérations sont dans une boîte noire et ne peuvent pas être optimisées.
LuaJIT FFI
Contrairement à FFI, la partie interactive de FFI est implémentée en Lua, qui peut être suivie par JIT et optimisée ; bien sûr, le code est également plus concis et plus facile à comprendre.
Prenons l'exemple de base64_decode
, dont l'implémentation FFI est répartie sur deux dépôts : lua-resty-core
et lua-nginx-module
, et regardons le code implémenté dans le premier.
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
Vous constaterez que par rapport à CFunction, le code de l'implémentation FFI est beaucoup plus clair, son implémentation spécifique est ngx_http_lua_ffi_decode_base64
dans le dépôt lua-nginx-module
. Si vous êtes intéressé ici, vous pouvez vérifier les performances de cette fonction vous-même. C'est très simple, je ne posterai pas le code ici.
Cependant, si vous êtes attentif, avez-vous remarqué certaines règles de nommage des fonctions dans l'extrait de code ci-dessus ?
Oui, toutes les fonctions dans OpenResty ont des conventions de nommage, et vous pouvez déduire leur utilisation par leur nom. Par exemple :
ngx_http_lua_ffi_
, la fonction Lua qui utilise FFI pour traiter les requêtes HTTP NGINX.ngx_http_lua_ngx_
, une fonction Lua qui utilise la fonction C pour traiter les requêtes HTTP NGINX.- Les autres fonctions commençant par ngx et lua sont respectivement des fonctions intégrées pour NGINX et Lua.
De plus, le code C dans OpenResty a une norme de code stricte, et je recommande de lire le guide de style de code C officiel ici. C'est un document indispensable pour les développeurs qui souhaitent apprendre le code C d'OpenResty et soumettre des PR. Sinon, même si votre PR est bien écrite, vous serez répété et demandé de la modifier en raison de problèmes de style de code.
Pour plus d'API et de détails sur FFI, nous vous recommandons de lire les tutoriels officiels de LuaJIT et la documentation. Les colonnes techniques ne peuvent pas remplacer la documentation officielle ; je ne peux que vous aider à indiquer le chemin d'apprentissage en un temps limité, avec moins de détours ; les problèmes difficiles doivent encore être résolus par vous.
LuaJIT FFI GC
Lorsque vous utilisez FFI, vous pouvez être confus : qui gère la mémoire demandée dans FFI ? Devons-nous la libérer manuellement en C, ou LuaJIT la récupère-t-il automatiquement ?
Voici un principe simple : LuaJIT n'est responsable que des ressources qu'il alloue lui-même ; ffi.
Par exemple, si vous demandez un bloc de mémoire en utilisant ffi.C.malloc
, vous devrez le libérer avec le ffi.C.free
associé. La documentation officielle de LuaJIT a un exemple équivalent.
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
...
p = nil -- La dernière référence à p est partie.
-- Le GC finira par exécuter le finaliseur : ffi.C.free(p)
Dans ce code, ffi.C.malloc(n)
demande une section de mémoire, et ffi.gc
enregistre une fonction de rappel de destruction ffi.C.free
, ffi.C.free
sera alors appelée automatiquement lorsqu'un cdata p
est GC par LuaJIT pour libérer la mémoire au niveau C. Et cdata est GC par LuaJIT. LuaJIT libérera automatiquement p
dans le code ci-dessus.
Notez que si vous souhaitez demander un gros bloc de mémoire dans OpenResty, je recommande d'utiliser ffi.C.malloc au lieu de ffi.new. Les raisons sont également évidentes.
ffi.new
retournecdata
, qui fait partie de la mémoire gérée par LuaJIT.- Le GC de LuaJIT a une limite supérieure de gestion de la mémoire, et LuaJIT dans OpenResty n'a pas l'option GC64 activée. Par conséquent, la limite supérieure de mémoire pour un seul worker est seulement de 2G. Une fois la limite supérieure de gestion de la mémoire de LuaJIT dépassée, cela provoquera une erreur.
Lorsque vous utilisez FFI, nous devons également faire particulièrement attention aux fuites de mémoire. Cependant, tout le monde fait des erreurs, et tant que le code est écrit par des humains, il y aura toujours des bugs.
C'est là que la robuste chaîne d'outils de test et de débogage d'OpenResty entre en jeu.
Parlons d'abord des tests. Dans le système OpenResty, nous utilisons Valgrind pour détecter les fuites de mémoire.
Le framework de test dont nous avons parlé dans le cours précédent, test::nginx
, a un mode spécial de détection des fuites de mémoire pour exécuter des ensembles de cas de test unitaires ; vous devez définir la variable d'environnement TEST_NGINX_USE_VALGRIND=1.
Le projet officiel OpenResty sera entièrement enregistré dans ce mode avant de publier la version, et nous entrerons dans plus de détails dans la section des tests plus tard. Nous entrerons dans plus de détails dans la section des tests plus tard.
Le CLI resty d'OpenResty a également l'option --valgrind
, qui vous permet d'exécuter un code Lua seul, même si vous n'avez pas écrit de cas de test.
Regardons les outils de débogage.
OpenResty fournit des extensions basées sur systemtap pour effectuer une analyse dynamique en direct des programmes OpenResty. Vous pouvez rechercher le mot-clé gc
dans l'ensemble d'outils de ce projet, et vous verrez deux outils, lj-gc
et lj-gc-objs
.
Pour l'analyse hors ligne comme core dump
, OpenResty fournit un ensemble d'outils GDB, et vous pouvez également rechercher gc
dedans et trouver les trois outils lgc
, lgcstat
et lgcpath
.
L'utilisation spécifique de ces outils de débogage sera couverte en détail dans la section de débogage plus tard, afin que vous puissiez en avoir une impression pour l'instant. Après tout, OpenResty dispose d'un ensemble d'outils dédiés pour vous aider à localiser et résoudre ces problèmes.
lua-resty-core
De la comparaison ci-dessus, nous pouvons voir que l'approche FFI est non seulement plus claire en termes de code, mais peut également être optimisée par LuaJIT, ce qui en fait le meilleur choix. OpenResty a déprécié l'implémentation CFunction, et la performance a été retirée de la base de code. Les nouvelles API sont maintenant implémentées dans le dépôt lua-resty-core
via FFI.
Avant la version 1.15.8.1 d'OpenResty publiée en mai 2019, lua-resty-core
n'était pas activé par défaut, ce qui entraînait des pertes de performance et des bugs potentiels, donc je recommande fortement à quiconque utilise encore la version historique d'activer manuellement lua-resty-core
. Vous n'avez qu'à ajouter une ligne de code à la phase init_by_lua
.
require "resty.core"
Bien sûr, la directive lua_load_resty_core a été ajoutée dans la version 1.15.8.1, et lua-resty-core
est activé par défaut.
Je pense personnellement qu'OpenResty est encore trop prudent concernant l'activation de lua-resty-core
, et les projets open source devraient activer des fonctionnalités similaires par défaut dès que possible.
lua-resty-core
non seulement réimplémente certaines API du projet lua-nginx-module
, comme ngx.re.match
, ngx.md5
, etc., mais implémente également plusieurs nouvelles API, comme ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.process
, et ngx.ngx.md5
. ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
, etc., que nous aborderons plus tard dans le chapitre sur les API OpenResty.
Résumé
Après tout cela, je voudrais conclure que FFI, bien que bon, n'est pas une solution miracle en termes de performance. La principale raison pour laquelle il est efficace est qu'il peut être suivi et optimisé par JIT. Si vous écrivez du code Lua qui ne peut pas être JIT et doit être exécuté en mode interprété, alors FFI sera moins efficace.
Alors, quelles opérations peuvent être JIT et lesquelles ne le peuvent pas ? Comment pouvons-nous éviter d'écrire du code qui ne peut pas être JIT ? Je révélerai cela dans la section suivante.
Enfin, un problème pratique à résoudre : Pouvez-vous trouver une ou deux API dans lua-nginx-module
et lua-resty-core
, puis comparer les différences dans les tests de performance ? Vous pourrez voir à quel point l'amélioration des performances de FFI est significative.
N'hésitez pas à laisser un commentaire, et je partagerai vos réflexions et vos acquis, et je vous invite à partager cet article avec vos collègues et amis, pour échanger et progresser ensemble.