Connaissances sur NGINX utilisées dans OpenResty

API7.ai

September 17, 2022

OpenResty (NGINX + Lua)

À travers l'article précédent, vous avez acquis une connaissance générale d'OpenResty. Dans les quelques articles suivants, je vais vous guider à travers les deux piliers d'OpenResty : NGINX et LuaJIT, et vous pourrez mieux apprendre OpenResty en maîtrisant ces bases.

Aujourd'hui, je vais commencer par NGINX, et ici, je ne présenterai que quelques bases de NGINX qui pourraient être utilisées dans OpenResty, ce qui ne représente qu'une infime partie de NGINX.

En ce qui concerne la configuration, dans le développement OpenResty, nous devons prêter attention aux points suivants.

  • Configurer nginx.conf le moins possible.
  • Éviter d'utiliser la combinaison de plusieurs directives telles que if, set, rewrite, etc.
  • Ne pas utiliser la configuration, les variables et les modules de NGINX lorsque vous pouvez résoudre le problème avec du code Lua.

Ces méthodes maximiseront la lisibilité, la maintenabilité et l'extensibilité. La configuration NGINX suivante est un exemple typique de mauvaise pratique consistant à utiliser la configuration comme du code.

location ~ ^/mobile/(web/app.htm) {
    set $type $1;
    set $orig_args $args;
    if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) {
        rewrite  ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
    }
    proxy_pass http://foo.com/$type?$orig_args;
}

C'est ce que nous devons éviter lors du développement avec OpenResty.

Configuration NGINX

NGINX contrôle son comportement via des fichiers de configuration, qui peuvent être considérés comme un simple DSL. NGINX lit la configuration au démarrage du processus et la charge en mémoire. Si vous modifiez le fichier de configuration, vous devrez redémarrer ou recharger NGINX et attendre que NGINX relise le fichier de configuration pour que la nouvelle configuration prenne effet. Seule la version commerciale de NGINX offre une partie de cette capacité dynamique au moment de l'exécution, sous forme d'API.

Commençons par la configuration suivante, qui est très simple.

worker_processes auto;

pid logs/nginx.pid;
error_log logs/error.log notice;

worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

http {
    server {
        listen 80;
        listen 443 ssl;

        location / {
            proxy_pass https://foo.com;
        }
    }
}

stream {
    server {
        listen 53 udp;
    }
}

Cependant, même ces configurations simples impliquent des concepts fondamentaux.

Premièrement, chaque directive a son contexte, qui est sa portée dans le fichier de configuration NGINX.

Le niveau supérieur est main, qui contient des instructions sans rapport avec une activité spécifique, comme worker_processes, pid, et error_log, qui font toutes partie du contexte main. En outre, il existe une relation hiérarchique entre les contextes. Par exemple, le contexte de location est server, le contexte de server est http, et le contexte de http est main.

Les directives ne peuvent pas être exécutées dans le mauvais contexte. NGINX vérifie si nginx.conf est légal au démarrage. Par exemple, si nous déplaçons listen 80; du contexte server au contexte main et démarrons le service NGINX, nous verrons une erreur comme celle-ci :

"listen" directive is not allowed here ......

Deuxièmement, NGINX peut gérer non seulement les requêtes HTTP et le trafic HTTPS, mais aussi le trafic UDP et TCP. Le L7 est dans HTTP et le L4 est dans Stream. Dans OpenResty, lua-nginx-module et stream-lua-nginx-module correspondent respectivement à ces deux aspects.

Une chose à noter ici est qu'OpenResty ne prend pas en charge toutes les fonctionnalités de NGINX, et vous devez vérifier la version d'OpenResty. La version d'OpenResty est cohérente avec celle de NGINX, ce qui facilite l'identification.

Les directives de configuration impliquées dans nginx.conf ci-dessus se trouvent dans les modules de base de NGINX ngx_core_module, ngx_http_core_module, et ngx_stream_core_module, que vous pouvez consulter pour voir la documentation spécifique.

Mode MASTER-WORKER

Après avoir compris le fichier de configuration, examinons le mode multi-processus de NGINX (comme illustré ci-dessous). Comme vous pouvez le voir, lorsque NGINX démarre, il y aura un processus Master et plusieurs processus Worker (ou un seul processus Worker, selon la configuration).

Mode Worker NGINX

Tout d'abord, le processus Master, comme son nom l'indique, joue le rôle de "manager" et n'est pas responsable de la gestion des requêtes des clients. Il gère le processus Worker, y compris la réception des signaux de l'administrateur et la surveillance de l'état des Worker. Lorsqu'un processus Worker se termine anormalement, le processus Master redémarre un nouveau processus Worker.

Les processus Worker sont les "véritables employés" qui gèrent les requêtes des clients. Ils sont créés à partir du processus Master et sont indépendants les uns des autres. Ce modèle multi-processus est bien plus avancé que le modèle multi-thread d'Apache, sans verrouillage inter-thread et facile à déboguer. Même si un processus plante et se termine, cela n'affecte généralement pas le travail des autres processus Worker.

OpenResty ajoute un agent privilégié unique au modèle Master-Worker de NGINX. Ce processus n'écoute aucun port et a les mêmes privilèges que le processus Master de NGINX, il peut donc effectuer des tâches nécessitant des privilèges élevés, comme certaines opérations d'écriture sur des fichiers locaux.

Si le processus privilégié fonctionne avec le mécanisme de mise à jour à chaud binaire de NGINX, OpenResty peut implémenter la mise à jour complète du binaire en cours d'exécution sans dépendre de programmes externes.

Réduire la dépendance aux programmes externes et essayer de résoudre les problèmes au sein du processus OpenResty facilite le déploiement, réduit les coûts d'exploitation et diminue la probabilité d'erreurs du programme. Le processus privilégié et ngx.pipe dans OpenResty sont tous deux destinés à cet effet.

Phase d'exécution

Les phases d'exécution sont également une caractéristique essentielle de NGINX et sont étroitement liées à l'implémentation spécifique d'OpenResty. NGINX a 11 phases d'exécution, que nous pouvons voir dans le code source de ngx_http_core_module.h :

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,

    NGX_HTTP_SERVER_REWRITE_PHASE,

    NGX_HTTP_FIND_CONFIG_PHASE,
    NGX_HTTP_REWRITE_PHASE,
    NGX_HTTP_POST_REWRITE_PHASE,

    NGX_HTTP_PREACCESS_PHASE,

    NGX_HTTP_ACCESS_PHASE,
    NGX_HTTP_POST_ACCESS_PHASE,

    NGX_HTTP_PRECONTENT_PHASE,

    NGX_HTTP_CONTENT_PHASE,

    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

Si vous souhaitez en savoir plus sur le rôle de ces 11 phases, vous pouvez lire la documentation de NGINX, donc je ne vais pas m'étendre ici.

Par coïncidence, OpenResty a également 11 directives *_by_lua liées à la phase NGINX, comme illustré ci-dessous (extrait de la documentation de lua-nginx-module).

Ordre des directives du module Lua NGINX

init_by_lua est exécuté uniquement lorsque le processus Master est créé, et init_worker_by_lua est exécuté uniquement lorsque chaque processus Worker est créé. Les autres commandes *_by_lua sont déclenchées par les requêtes des clients et sont exécutées de manière répétée.

Ainsi, pendant la phase init_by_lua, nous pouvons précharger les modules Lua et les données publiques en lecture seule pour tirer parti de la fonctionnalité COW (copy on write) du système d'exploitation et économiser de la mémoire.

La plupart des opérations peuvent être effectuées dans content_by_lua, mais je recommande de les diviser selon différentes fonctions, comme suit.

  • set_by_lua : définition des variables.
  • rewrite_by_lua : redirection, réécriture, etc.
  • access_by_lua : accès, permissions, etc.
  • content_by_lua : génération du contenu de retour.
  • header_filter_by_lua : traitement du filtrage des en-têtes de réponse.
  • body_filter_by_lua : traitement du filtrage du corps de la réponse.
  • log_by_lua : journalisation.

Prenons un exemple pour montrer les avantages de cette division. Supposons que de nombreuses API en texte clair soient fournies à l'extérieur, et que nous devions maintenant ajouter une logique de chiffrement et de déchiffrement personnalisée. Devons-nous modifier le code de toutes les API ?

location /mixed {
    content_by_lua '...';
}

Bien sûr que non. En utilisant la fonctionnalité de phase, nous pouvons déchiffrer dans la phase access et chiffrer dans la phase body filter sans apporter de modifications au code dans la phase content d'origine.

location /mixed {
    access_by_lua '...';
    content_by_lua '...';
    body_filter_by_lua '...';
}

Mise à jour à chaud du binaire NGINX

Enfin, permettez-moi d'expliquer brièvement la mise à jour à chaud du binaire NGINX. Nous savons qu'après avoir modifié le fichier de configuration NGINX, vous devez le recharger pour qu'il prenne effet. Mais lorsque NGINX se met à jour lui-même, il peut le faire à chaud. Cela peut sembler mettre la charrue avant les bœufs, mais c'est compréhensible étant donné que NGINX a commencé avec l'équilibrage de charge statique traditionnel, le proxy inverse et la mise en cache de fichiers.

La mise à jour à chaud est effectuée en envoyant les signaux USR2 et WINCH à l'ancien processus Master. Pour ces deux étapes, la première démarre le nouveau processus Master ; la seconde arrête progressivement le processus Worker.

Après ces deux étapes, le nouveau Master et le nouveau Worker sont démarrés. À ce stade, l'ancien Master ne se termine pas. La raison de ne pas quitter est simple : si vous avez besoin de revenir en arrière, vous pouvez toujours envoyer des signaux HUP à l'ancien Master. Bien sûr, si vous avez déterminé que vous n'avez pas besoin de revenir en arrière, vous pouvez envoyer un signal KILL à l'ancien Master pour qu'il se termine.

C'est tout, et la mise à jour à chaud du binaire NGINX est terminée.

Si vous souhaitez en savoir plus sur ce sujet, vous pouvez consulter la documentation officielle pour continuer à apprendre.

Résumé

En général, ce que vous utilisez dans OpenResty sont les bases de NGINX, principalement liées à la configuration, aux processus maître-esclave, aux phases d'exécution, etc. Les autres choses qui peuvent être résolues avec du code Lua sont résolues avec du code autant que possible, plutôt que d'utiliser des modules et des configurations NGINX, ce qui représente un changement de mentalité lors de l'apprentissage d'OpenResty.

Enfin, je vous laisse avec une question ouverte : Nginx prend officiellement en charge NJS, ce qui signifie que vous pouvez écrire du JS pour contrôler une partie de la logique NGINX, similaire à OpenResty. Que pensez-vous de cela ? N'hésitez pas à partager cet article.