Au-delà du serveur Web : processus privilégiés et tâches de minuterie
API7.ai
November 3, 2022
Dans l'article précédent, nous avons présenté les API OpenResty, shared dict
et cosocket
, qui implémentent des fonctionnalités dans le domaine de NGINX et des serveurs web, fournissant un serveur web programmable qui est moins coûteux et plus facile à maintenir.
Cependant, OpenResty peut faire bien plus que cela. Aujourd'hui, nous allons explorer quelques fonctionnalités d'OpenResty qui vont au-delà du serveur web et les présenter. Il s'agit des tâches planifiées (timer tasks), du processus privilégié (privileged process), et du ngx.pipe
non bloquant.
Tâches Planifiées (Timer Tasks)
Dans OpenResty, nous avons parfois besoin d'exécuter régulièrement des tâches spécifiques en arrière-plan, comme la synchronisation de données, le nettoyage des logs, etc. Si vous deviez concevoir cela, comment le feriez-vous ? La manière la plus simple serait de fournir une interface API pour exécuter ces tâches, puis d'utiliser le crontab
du système pour appeler curl
à intervalles réguliers afin d'accéder à cette interface, et ainsi implémenter cette exigence de manière indirecte.
Cependant, cela ne serait pas seulement fragmenté, mais apporterait également une complexité accrue à l'exploitation et à la maintenance. Ainsi, OpenResty fournit ngx.timer
pour résoudre ce type de besoin. Vous pouvez considérer ngx.timer
comme une requête client simulée par OpenResty pour déclencher la fonction de rappel correspondante.
Les tâches planifiées d'OpenResty peuvent être divisées en deux types :
ngx.timer.at
est utilisé pour exécuter des tâches planifiées ponctuelles.ngx.timer.every
est utilisé pour exécuter des tâches planifiées à intervalles fixes.
Vous souvenez-vous de la question que j'ai laissée à la fin du dernier article ? La question était de savoir comment contourner la restriction selon laquelle cosocket
ne peut pas être utilisé dans init_worker_by_lua
, et la réponse est ngx.timer
.
Le code suivant démarre une tâche planifiée avec un délai de 0
. Il démarre la fonction de rappel handler
, et dans cette fonction, il utilise cosocket
pour accéder à un site web.
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“api7.ai", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
Ainsi, nous contournons la restriction selon laquelle cosocket
ne peut pas être utilisé à ce stade.
Revenons au besoin utilisateur mentionné au début de cette section, ngx.timer.at
ne répond pas au besoin d'exécution périodique ; dans l'exemple de code ci-dessus, il s'agit d'une tâche ponctuelle.
Alors, comment faire cela périodiquement ? Vous semblez avoir deux options basées sur l'API ngx.timer.at
.
- Vous pouvez implémenter la tâche périodique vous-même en utilisant une boucle infinie
while true
dans la fonction de rappel quisleep
pendant un certain temps après l'exécution de la tâche. - Vous pouvez également créer un nouveau timer à la fin de la fonction de rappel.
Cependant, avant de faire un choix, il y a une chose que nous devons clarifier : le timer est essentiellement une requête, bien que la requête n'ait pas été initiée par le client. Pour la requête, elle doit se terminer après avoir accompli sa tâche et ne peut pas rester résidente. Sinon, cela peut facilement causer diverses fuites de ressources.
Par conséquent, la première solution consistant à utiliser while true
pour implémenter des tâches périodiques n'est pas fiable. La deuxième solution est faisable mais crée des timers de manière récursive, ce qui n'est pas facile à comprendre.
Alors, existe-t-il une meilleure solution ? La nouvelle API ngx.timer.every
d'OpenResty est spécifiquement conçue pour résoudre ce problème, et c'est une solution plus proche de crontab
.
L'inconvénient est que vous n'avez jamais la possibilité d'annuler une tâche planifiée après l'avoir démarrée. Après tout, ngx.timer.cancel
est toujours une fonction à faire.
À ce stade, vous rencontrerez un problème : le timer fonctionne en arrière-plan et ne peut pas être annulé ; s'il y a beaucoup de timers, il est facile d'épuiser les ressources du système.
Par conséquent, OpenResty fournit deux directives, lua_max_pending_timers
et lua_max_running_timers
pour les limiter. La première représente le nombre maximum de timers en attente d'exécution, et la seconde représente le nombre maximum de timers actuellement en cours d'exécution.
Vous pouvez également utiliser l'API Lua pour obtenir les valeurs des tâches planifiées en attente et en cours d'exécution, comme le montrent les deux exemples suivants.
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
Ce code imprimera un 1
, indiquant qu'il y a une tâche planifiée en attente d'exécution.
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
Ce code imprimera un 1
, indiquant qu'il y a une tâche planifiée en cours d'exécution.
Processus Privilégié (Privileged Process)
Ensuite, examinons le processus privilégié. Comme nous le savons tous, NGINX est divisé en processus Master
et processus Worker
, où les processus worker traitent les requêtes des utilisateurs. Nous pouvons obtenir le type de processus via l'API process.type
fournie dans lua-resty-core
. Par exemple, vous pouvez utiliser resty
pour exécuter la fonction suivante.
$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'
Vous verrez qu'il retourne un résultat single
au lieu de worker
, ce qui signifie que resty
démarre NGINX avec un processus Worker
, et non un processus Master
. C'est vrai. Dans l'implémentation de resty
, vous pouvez voir que le processus Master
est désactivé avec une ligne comme celle-ci.
master_process off;
OpenResty étend NGINX en ajoutant un privileged agent
. Le processus privilégié a les caractéristiques spéciales suivantes.
-
Il ne surveille aucun port, ce qui signifie qu'il ne fournit pas de services à l'extérieur.
-
Il a les mêmes privilèges que le processus
Master
, qui est généralement le privilège de l'utilisateurroot
, lui permettant d'effectuer de nombreuses tâches impossibles pour le processusWorker
. -
Le processus privilégié ne peut être ouvert que dans le contexte
init_by_lua
. -
De plus, le processus privilégié n'a de sens que s'il s'exécute dans le contexte
init_worker_by_lua
car aucune requête n'est déclenchée, et il ne va pas dans les contextescontent
,access
, etc.
Regardons un exemple de processus privilégié activé.
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent()
if not ok then
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
end
}
Après avoir activé le processus privilégié avec ce code et démarré le service OpenResty, nous pouvons voir que le processus privilégié fait maintenant partie du processus NGINX.
nginx: master process
nginx: worker process
nginx: privileged agent process
Cependant, si les privilèges ne sont exécutés qu'une seule fois pendant la phase init_worker_by_lua
, ce qui n'est pas une bonne idée, comment devrions-nous déclencher le processus privilégié ?
Oui, la réponse est cachée dans les connaissances que nous venons d'apprendre. Puisqu'il n'écoute pas les ports, c'est-à-dire qu'il ne peut pas être déclenché par des requêtes terminales, la seule façon de le déclencher périodiquement est d'utiliser le ngx.timer
que nous venons d'introduire :
init_worker_by_lua_block {
local process = require "ngx.process"
local function reload(premature)
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
if not f then
return
end
local pid = f:read()
f:close()
os.execute("kill -HUP " .. pid)
end
if process.type() == "privileged agent" then
local ok, err = ngx.timer.every(5, reload)
if not ok then
ngx.log(ngx.ERR, err)
end
end
}
Le code ci-dessus implémente la capacité d'envoyer des signaux HUP
au processus master toutes les 5 secondes. Naturellement, vous pouvez vous appuyer sur cela pour faire des choses plus excitantes, comme interroger la base de données pour voir s'il y a des tâches pour le processus privilégié et les exécuter. Puisque le processus privilégié a les privilèges root
, c'est évidemment un peu un programme "backdoor".
ngx.pipe
Non Bloquant
Enfin, examinons le ngx.pipe
non bloquant, qui utilise la bibliothèque standard Lua pour exécuter une commande externe qui envoie un signal au processus Master
dans l'exemple de code que nous venons de décrire.
os.execute("kill -HUP " .. pid)
Naturellement, cette opération sera bloquante. Alors, existe-t-il une manière non bloquante d'appeler des programmes externes dans OpenResty ? Après tout, vous savez que si vous utilisez OpenResty comme une plateforme de développement complète et non comme un serveur web, c'est ce dont vous avez besoin. Pour cette raison, la bibliothèque lua-resty-shell
a été créée, et l'utiliser pour invoquer la ligne de commande est non bloquant :
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout)
Ce code est une manière différente d'écrire hello world
, en appelant la commande echo
du système pour compléter la sortie. De même, vous pouvez utiliser resty.shell
comme alternative à l'appel os.execute
en Lua.
Nous savons que l'implémentation sous-jacente de lua-resty-shell
repose sur l'API ngx.pipe
dans lua-resty-core
, donc cet exemple utilise lua-resty-shell
pour imprimer hello world
, en utilisant ngx.pipe
à la place, cela ressemblerait à ceci.
$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'
Ce qui précède est le code sous-jacent de l'implémentation de lua-resty-shell
. Vous pouvez consulter la documentation de ngx.pipe
et les cas de test pour plus d'informations sur son utilisation. Par conséquent, je ne vais pas m'y attarder ici.
Résumé
C'est tout. Nous avons terminé le contenu principal d'aujourd'hui. À partir des fonctionnalités ci-dessus, nous pouvons voir qu'OpenResty essaie également de se rapprocher de la direction d'une plateforme universelle tout en améliorant NGINX, espérant que les développeurs pourront essayer d'unifier la pile technologique et utiliser OpenResty pour répondre à leurs besoins de développement. C'est assez convivial pour les opérations et la maintenance car les coûts de maintenance sont plus bas tant que vous déployez un OpenResty dessus.
Enfin, je vous laisse avec une question stimulante. Puisqu'il peut y avoir plusieurs Worker
NGINX, le timer
s'exécutera une fois pour chaque Worker
, ce qui est inacceptable dans la plupart des scénarios. Comment pouvons-nous nous assurer que le timer
ne s'exécute qu'une seule fois ?
N'hésitez pas à laisser un commentaire avec votre solution, et n'hésitez pas à partager cet article avec vos collègues et amis afin que nous puissions communiquer et nous améliorer ensemble.