Qu'est-ce qui rend OpenResty si spécial ?
API7.ai
October 14, 2022
Dans les articles précédents, vous avez appris les deux pierres angulaires d'OpenResty : NGINX et LuaJIT, et je suis sûr que vous êtes prêt à commencer à apprendre les API fournies par OpenResty.
Mais ne soyez pas trop pressé. Avant de le faire, vous devez passer un peu plus de temps à vous familiariser avec les principes et les concepts de base d'OpenResty.
Principes
Les processus Master
et Worker
d'OpenResty contiennent tous deux une VM LuaJIT, qui est partagée par toutes les coroutines au sein du même processus, et dans laquelle le code Lua est exécuté.
Et à un moment donné, chaque processus Worker
ne peut traiter que les requêtes d'un seul utilisateur, ce qui signifie qu'une seule coroutine est en cours d'exécution. Vous pourriez vous poser une question : Puisque NGINX peut supporter C10K (dizaines de milliers de concurrences), n'a-t-il pas besoin de traiter 10 000 requêtes simultanément ?
Bien sûr que non. NGINX utilise epoll
pour piloter les événements afin de réduire l'attente et l'inactivité, de sorte que le plus de ressources CPU possible soient utilisées pour traiter les requêtes des utilisateurs. Après tout, l'ensemble ne parvient à une haute performance que lorsque les requêtes individuelles sont traitées suffisamment rapidement. Si un mode multi-thread est utilisé de sorte qu'une requête corresponde à un thread, alors avec C10K, les ressources peuvent facilement être épuisées.
Au niveau d'OpenResty, les coroutines de Lua fonctionnent en conjonction avec le mécanisme d'événements de NGINX. Si une opération d'I/O comme l'interrogation d'une base de données MySQL se produit dans le code Lua, elle appellera d'abord le yield
de la coroutine Lua pour se suspendre, puis enregistrera un callback dans NGINX ; après que l'opération d'I/O soit terminée (ce qui pourrait aussi être un timeout ou une erreur), le callback resume
de NGINX réveillera la coroutine Lua. Cela complète la coopération entre la concurrence Lua et les pilotes d'événements NGINX, évitant d'écrire des callbacks dans le code Lua.
Nous pouvons regarder le diagramme suivant, qui décrit l'ensemble du processus. lua_yield
et lua_resume
font tous deux partie de la lua_CFunction
fournie par Lua.
D'un autre côté, s'il n'y a pas d'opérations d'I/O ou de sleep
dans le code Lua, comme toutes les opérations intensives de chiffrement et de déchiffrement, alors la VM LuaJIT sera occupée par la coroutine Lua jusqu'à ce que l'ensemble de la requête soit traité.
J'ai fourni un extrait de code source pour ngx.sleep
ci-dessous pour vous aider à comprendre cela plus clairement. Ce code se trouve dans ngx_http_lua_sleep.c
, que vous pouvez trouver dans le répertoire src
du projet lua-nginx-module
.
Dans ngx_http_lua_sleep.c
, nous pouvons voir l'implémentation concrète de la fonction sleep
. Vous devez d'abord enregistrer l'API Lua ngx.sleep
avec la fonction C ngx_http_lua_ngx_sleep
.
void ngx_http_lua_inject_sleep_api(lua_State *L)
{
lua_pushcfunction(L, ngx_http_lua_ngx_sleep);
lua_setfield(L, -2, "sleep");
}
Voici la fonction principale de sleep
, et j'ai extrait seulement quelques lignes du code principal ici.
static int ngx_http_lua_ngx_sleep(lua_State *L)
{
coctx->sleep.handler = ngx_http_lua_sleep_handler;
ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
return lua_yield(L, 0);
}
Comme vous pouvez le voir :
- Ici, la fonction de rappel
ngx_http_lua_sleep_handler
est d'abord ajoutée. - Ensuite, appelez
ngx_add_timer
, une interface fournie par NGINX, pour ajouter un minuteur à la boucle d'événements de NGINX. - Enfin, utilisez
lua_yield
pour suspendre la concurrence Lua, en cédant le contrôle à la boucle d'événements de NGINX.
La fonction de rappel ngx_http_lua_sleep_handler
est déclenchée lorsque l'opération de sommeil est terminée. Elle appelle ngx_http_lua_sleep_resume
et finit par réveiller la coroutine Lua en utilisant lua_resume
. Vous pouvez récupérer les détails de l'appel vous-même dans le code afin que je n'entre pas dans les détails ici.
ngx.sleep
est juste l'exemple le plus simple, mais en le disséquant, vous pouvez voir les principes de base du module lua-nginx-module
.
Concepts de base
Après avoir analysé les principes, rafraîchissons notre mémoire et rappelons-nous les deux concepts importants des phases et du non-blocage dans OpenResty.
OpenResty, comme NGINX, a le concept de phases, et chaque phase a son propre rôle distinct :
set_by_lua
, qui est utilisé pour définir des variables.rewrite_by_lua
, pour le transfert, la redirection, etc.access_by_lua
, pour l'accès, les permissions, etc.content_by_lua
, pour générer le contenu de retour.header_filter_by_lua
, pour le traitement du filtre d'en-tête de réponse.body_filter_by_lua
, pour le filtrage du corps de la réponse.log_by_lua
, pour la journalisation.
Bien sûr, si la logique de votre code n'est pas trop complexe, il est possible de l'exécuter entièrement dans la phase rewrite
ou content
.
Cependant, notez que les API d'OpenResty ont des limites d'utilisation des phases. Chaque API a une liste de phrases dans lesquelles elle peut être utilisée, et vous obtiendrez une erreur si vous l'utilisez hors de portée. Cela est très différent des autres langages de développement.
À titre d'exemple, j'utiliserai ngx.sleep
. D'après la documentation, je sais qu'il ne peut être utilisé que dans les contextes suivants et n'inclut pas la phase log
.
context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
Et si vous ne le savez pas, utilisez sleep
dans une phase log
qu'il ne supporte pas :
location / {
log_by_lua_block {
ngx.sleep(1)
}
}
Dans le journal d'erreurs de NGINX, il y a une indication de niveau error
.
[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua*
stack traceback:
[C]: in function 'sleep'
Donc, avant d'utiliser l'API, n'oubliez pas de consulter la documentation pour déterminer si elle peut être utilisée dans le contexte de votre code.
Après avoir revu le concept de phases, revoyons le non-blocage. Tout d'abord, clarifions que toutes les API fournies par OpenResty sont non bloquantes.
Je continuerai avec l'exigence de sleep 1 seconde comme exemple. Si vous voulez l'implémenter en Lua, vous devez faire ceci.
function sleep(s)
local ntime = os.time() + s
repeat until os.time() > ntime
end
Puisque Lua standard n'a pas de fonction sleep
, j'utilise ici une boucle pour continuer à déterminer si le temps spécifié a été atteint. Cette implémentation est bloquante, et pendant la seconde où sleep
est en cours d'exécution, Lua ne fait rien pendant que d'autres requêtes qui doivent être traitées attendent simplement.
Cependant, si nous passons à ngx.sleep(1)
, selon le code source que nous avons analysé ci-dessus, OpenResty peut toujours traiter d'autres requêtes (comme request B
) pendant cette seconde. Le contexte de la requête actuelle (appelons-la request A
) sera sauvegardé et réveillé par le mécanisme d'événements de NGINX, puis reviendra à request A
, de sorte que le CPU est toujours dans un état de travail naturel.
Variables et cycle de vie
En plus de ces deux concepts importants, le cycle de vie des variables est également une zone facile à mal comprendre dans le développement d'OpenResty.
Comme je l'ai dit précédemment, dans OpenResty, je vous recommande de déclarer toutes les variables comme des variables locales et d'utiliser des outils comme luacheck
et lua-releng
pour détecter les variables globales. C'est la même chose pour les modules, comme ci-dessous.
local ngx_re = require "ngx.re"
Dans OpenResty, à l'exception des deux phases init_by_lua
et init_worker_by_lua
, une table isolée de variables globales est définie pour toutes les phases afin d'éviter de contaminer d'autres requêtes pendant le traitement. Même dans ces deux phases où vous pouvez définir des variables globales, vous devriez essayer d'éviter de le faire.
En règle générale, les problèmes qui tentent d'être résolus avec des variables globales devraient être mieux résolus avec des variables dans des modules et seront beaucoup plus clairs. Voici un exemple de variable dans un module.
local _M = {}
_M.color = {
red = 1,
blue = 2,
green = 3
}
return _M
J'ai défini un module dans un fichier appelé hello.lua
, qui contient la table color
, puis j'ai ajouté la configuration suivante à nginx.conf
.
location / {
content_by_lua_block {
local hello = require "hello"
ngx.say(hello.color.green)
}
}
Cette configuration exigera le module dans la phase content
et imprimera la valeur de green
comme corps de réponse HTTP.
Vous pourriez vous demander pourquoi la variable de module est si incroyable ?
Le module ne sera chargé qu'une seule fois dans le même processus Worker
; après cela, toutes les requêtes traitées par le Worker
partageront les données du module. Nous disons que les données "globales" sont adaptées à l'encapsulation dans des modules parce que les Worker
d'OpenResty sont entièrement isolés les uns des autres, donc chaque Worker
charge le module indépendamment, et les données du module ne peuvent pas traverser les Worker
.
Quant à la gestion des données qui doivent être partagées entre les Worker
, je laisserai cela pour un chapitre ultérieur, donc vous n'avez pas à creuser ici.
Cependant, il y a une chose qui peut mal tourner ici : lors de l'accès aux variables de module, il est préférable de les garder en lecture seule et de ne pas essayer de les modifier, sinon vous obtiendrez une race
dans le cas d'une haute concurrence, un bug qui ne peut pas être détecté par les tests unitaires, qui se produit occasionnellement en ligne et est difficile à localiser.
Par exemple, la valeur actuelle de la variable de module green
est 3
, et vous faites une opération plus 1
dans votre code, donc la valeur de green
est-elle maintenant 4
? Pas nécessairement ; cela pourrait être 4
, 5
ou 6
parce qu'OpenResty ne verrouille pas lors de l'écriture dans une variable de module. Ensuite, il y a compétition, et la valeur de la variable de module est mise à jour par plusieurs requêtes simultanément.
Ayant dit cela sur les variables globales, locales et de module, discutons des variables inter-phases.
Il y a des situations où nous avons besoin de variables qui traversent les phases et peuvent être lues et écrites. Des variables comme $host
, $scheme
, etc., qui nous sont familières dans NGINX, ne peuvent pas être créées dynamiquement même si elles satisfont à la condition inter-phases, et vous devez les définir dans le fichier de configuration avant de pouvoir les utiliser. Par exemple, si vous écrivez quelque chose comme ce qui suit.
location /foo {
set $my_var ; # besoin de créer la variable $my_var d'abord
content_by_lua_block {
ngx.var.my_var = 123
}
}
OpenResty fournit ngx.ctx
pour résoudre ce genre de problème. C'est une table Lua qui peut être utilisée pour stocker des données Lua basées sur la requête avec la même durée de vie que la requête actuelle. Regardons cet exemple de la documentation officielle.
location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}
Vous pouvez voir que nous avons défini une variable foo
qui est stockée dans ngx.ctx
. Cette variable traverse les phases rewrite
, access
et content
et finit par imprimer la valeur dans la phase content
, qui est 79
comme nous l'attendions.
Bien sûr, ngx.ctx
a ses limites.
Par exemple, les requêtes enfants créées avec ngx.location.capture
auront leurs propres données ngx.ctx
séparées, indépendantes des données ngx.ctx
de la requête parent.
Ensuite, les redirections internes créées avec ngx.exec
détruisent le ngx.ctx
de la requête originale et le régénèrent avec un ngx.ctx
vierge.
Ces deux limites ont des exemples de code détaillés dans la documentation officielle, donc vous pouvez les vérifier vous-même si vous êtes intéressé.
Résumé
Enfin, je dirai quelques mots de plus. Nous apprenons les principes d'OpenResty et quelques concepts importants, mais vous n'avez pas besoin de les mémoriser. Après tout, ils ont toujours un sens et prennent vie lorsqu'ils sont combinés avec des exigences et du code réels.
Je me demande comment vous le comprenez ? N'hésitez pas à laisser un commentaire et à discuter avec moi, et n'hésitez pas non plus à partager cet article avec vos collègues et amis. Nous communiquons ensemble, ensemble avec progrès.