Au-delà du serveur Web : processus privilégiés et tâches de minuterie

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

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 qui sleep 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'utilisateur root, lui permettant d'effectuer de nombreuses tâches impossibles pour le processus Worker.

  • 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 contextes content, 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.