Compréhension des algorithmes de sélection de serveur et de bloc de localisation Nginx

Introduction

Nginx est l’un des serveurs web les plus populaires au monde. Il peut gérer efficacement de fortes charges avec de nombreuses connexions clientes simultanées et peut fonctionner comme un serveur web, un serveur de messagerie ou un serveur proxy inverse.

Dans ce guide, nous discuterons de certains détails en coulisses qui déterminent comment Nginx traite les demandes des clients. Comprendre ces idées peut aider à éliminer les conjectures lors de la conception de blocs de serveurs et d’emplacements, et peut rendre le traitement des demandes moins imprévisible.

Configurations de blocs Nginx

Nginx divise logiquement les configurations destinées à servir différents contenus en blocs, qui vivent dans une structure hiérarchique. Chaque fois qu’une demande de client est faite, Nginx commence un processus pour déterminer quels blocs de configuration doivent être utilisés pour traiter la demande. Ce processus de décision est ce que nous discuterons dans ce guide.

Les principaux blocs que nous discuterons sont le bloc serveur et le bloc emplacement.

A server block is a subset of Nginx’s configuration that defines a virtual server used to handle requests of a defined type. Administrators often configure multiple server blocks and decide which block should handle which connection based on the requested domain name, port, and IP address.

A location block lives within a server block and is used to define how Nginx should handle requests for different resources and URIs for the parent server. The URI space can be subdivided in whatever way the administrator likes using these blocks. It is an extremely flexible model.

Comment Nginx décide quel bloc serveur gérera une requête

Étant donné que Nginx permet à l’administrateur de définir plusieurs blocs serveur qui fonctionnent comme des instances de serveur web virtuel distinctes, il a besoin d’une procédure pour déterminer lequel de ces blocs serveur sera utilisé pour satisfaire une requête.

Il le fait grâce à un système de contrôles défini qui sont utilisés pour trouver la meilleure correspondance possible. Les principales directives de bloc serveur dont Nginx se préoccupe pendant ce processus sont la directive listen et la directive server_name.

Analyse de la directive listen pour trouver des correspondances possibles

Tout d’abord, Nginx examine l’adresse IP et le port de la requête. Il fait correspondre cela avec la directive listen de chaque serveur pour construire une liste des blocs serveur qui peuvent potentiellement résoudre la requête.

La directive listen définit généralement l’adresse IP et le port auxquels le bloc serveur répondra. Par défaut, tout bloc serveur qui ne comprend pas de directive listen se voit attribuer les paramètres d’écoute 0.0.0.0:80 (ou 0.0.0.0:8080 si Nginx est exécuté par un utilisateur normal, non-root). Cela permet à ces blocs de répondre aux requêtes sur n’importe quelle interface sur le port 80, mais cette valeur par défaut ne pèse pas beaucoup dans le processus de sélection du serveur.

La directive listen peut être définie comme suit :

  • Une combinaison adresse IP/port.
  • A lone IP address which will then listen on the default port 80.
  • A lone port which will listen to every interface on that port.
  • Le chemin vers un socket Unix.

La dernière option aura généralement des implications uniquement lors du transfert de requêtes entre différents serveurs.

Lors de la tentative de détermination du bloc serveur auquel envoyer une requête, Nginx essaiera d’abord de décider en fonction de la spécificité de la directive listen en utilisant les règles suivantes :

  • Nginx traduit toutes les directives listen « incomplètes » en substituant les valeurs manquantes par leurs valeurs par défaut afin que chaque bloc puisse être évalué par son adresse IP et son port. Voici quelques exemples de ces traductions :
    • Un bloc sans directive listen utilise la valeur 0.0.0.0:80.
    • Un bloc défini sur une adresse IP 111.111.111.111 sans port devient 111.111.111.111:80
    • Un bloc défini sur le port 8888 sans adresse IP devient 0.0.0.0:8888
  • Ensuite, Nginx tente de collecter une liste des blocs serveurs qui correspondent le plus spécifiquement à la requête en fonction de l’adresse IP et du port. Cela signifie que tout bloc utilisant fonctionnellement 0.0.0.0 comme adresse IP (pour correspondre à n’importe quelle interface) ne sera pas sélectionné s’il existe des blocs correspondants qui répertorient une adresse IP spécifique. En tout cas, le port doit correspondre exactement.
  • S’il n’y a qu’une seule correspondance la plus spécifique, ce bloc serveur sera utilisé pour répondre à la demande. S’il existe plusieurs blocs serveurs avec le même niveau de spécificité, Nginx commence alors à évaluer la directive server_name de chaque bloc serveur.

Il est important de comprendre que Nginx n’évaluera la directive server_name que lorsqu’il devra distinguer entre les blocs serveurs qui correspondent au même niveau de spécificité dans la directive listen. Par exemple, si example.com est hébergé sur le port 80 de 192.168.1.10, une demande pour example.com sera toujours servie par le premier bloc dans cet exemple, malgré la directive server_name dans le deuxième bloc.

server {
    listen 192.168.1.10;

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

Dans le cas où plus d’un bloc serveur correspond avec une égale spécificité, la prochaine étape consiste à vérifier la directive server_name.

Analyse de la directive server_name pour choisir une correspondance

Ensuite, pour évaluer davantage les requêtes qui ont des directives listen également spécifiques, Nginx vérifie l’en-tête Host de la requête. Cette valeur contient le domaine ou l’adresse IP que le client essayait réellement d’atteindre.

Nginx tente de trouver la meilleure correspondance pour la valeur qu’il trouve en examinant la directive server_name dans chacun des blocs serveur qui sont encore des candidats à la sélection. Nginx évalue ceux-ci en utilisant la formule suivante:

  • Nginx va d’abord essayer de trouver un bloc serveur avec un server_name qui correspond exactement à la valeur dans l’en-tête Host de la requête exactement. Si cela est trouvé, le bloc associé sera utilisé pour servir la requête. Si plusieurs correspondances exactes sont trouvées, la première sera utilisée.
  • Si aucune correspondance exacte n’est trouvée, Nginx tentera alors de trouver un bloc serveur avec un server_name correspondant en utilisant un joker en tête (indiqué par un * au début du nom dans la configuration). Si une correspondance est trouvée, ce bloc sera utilisé pour servir la requête. Si plusieurs correspondances sont trouvées, la plus longue sera utilisée pour servir la requête.
  • Si aucune correspondance n’est trouvée en utilisant un joker en tête, Nginx recherche ensuite un bloc serveur avec un server_name correspondant en utilisant un joker en queue (indiqué par un nom de serveur se terminant par un * dans la configuration). Si une correspondance est trouvée, ce bloc est utilisé pour servir la requête. Si plusieurs correspondances sont trouvées, la plus longue sera utilisée pour servir la requête.
  • Si aucune correspondance n’est trouvée en utilisant un joker en fin de motif, Nginx évalue ensuite les blocs serveur qui définissent le server_name en utilisant des expressions régulières (indiqué par un ~ avant le nom). Le premier server_name avec une expression régulière qui correspond à l’en-tête « Host » sera utilisé pour traiter la requête.
  • Si aucune correspondance avec une expression régulière n’est trouvée, Nginx sélectionne ensuite le bloc serveur par défaut pour cette adresse IP et ce port.

Chaque combinaison adresse IP/port a un bloc serveur par défaut qui sera utilisé lorsque aucune action ne peut être déterminée avec les méthodes ci-dessus. Pour une combinaison adresse IP/port, cela sera soit le premier bloc dans la configuration, soit le bloc qui contient l’option default_server dans la directive listen (ce qui remplacerait l’algorithme de premier trouvé). Il ne peut y avoir qu’une seule déclaration default_server par combinaison adresse IP/port.

Exemples

S’il existe un server_name défini qui correspond exactement à la valeur de l’en-tête Host, ce bloc serveur est sélectionné pour traiter la requête.

Dans cet exemple, si l’en-tête Host de la requête était défini sur host1.example.com, le deuxième serveur serait sélectionné :

server {
    listen 80;
    server_name *.example.com;

    . . .

}

server {
    listen 80;
    server_name host1.example.com;

    . . .

}

Si aucune correspondance exacte n’est trouvée, Nginx vérifie ensuite s’il existe un server_name avec un joker de début qui correspond. La correspondance la plus longue commençant par un joker sera sélectionnée pour répondre à la demande.

Dans cet exemple, si la demande avait un en-tête Host de www.example.org, le deuxième bloc de serveurs serait sélectionné :

server {
    listen 80;
    server_name www.example.*;

    . . .

}

server {
    listen 80;
    server_name *.example.org;

    . . .

}

server {
    listen 80;
    server_name *.org;

    . . .

}

Si aucune correspondance n’est trouvée avec un joker de début, Nginx vérifiera ensuite si une correspondance existe en utilisant un joker à la fin de l’expression. À ce stade, la correspondance la plus longue se terminant par un joker sera sélectionnée pour traiter la demande.

Par exemple, si la demande a un en-tête Host défini sur www.example.com, le troisième bloc de serveurs sera sélectionné :

server {
    listen 80;
    server_name host1.example.com;

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

server {
    listen 80;
    server_name www.example.*;

    . . .

}

Si aucune correspondance avec des jokers ne peut être trouvée, Nginx passera alors à la tentative de correspondance des directives server_name utilisant des expressions régulières. La première expression régulière correspondante sera sélectionnée pour répondre à la demande.

Par exemple, si l’en-tête Host de la demande est défini sur www.example.com, alors le deuxième bloc de serveurs sera sélectionné pour satisfaire la demande :

server {
    listen 80;
    server_name example.com;

    . . .

}

server {
    listen 80;
    server_name ~^(www|host1).*\.example\.com$;

    . . .

}

server {
    listen 80;
    server_name ~^(subdomain|set|www|host1).*\.example\.com$;

    . . .

}

Si aucune des étapes ci-dessus ne parvient à satisfaire la demande, alors la demande sera transmise au serveur par défaut pour l’adresse IP et le port correspondants.

Blocs de localisation correspondants

De manière similaire au processus utilisé par Nginx pour sélectionner le bloc serveur qui traitera une requête, Nginx dispose également d’un algorithme établi pour décider quel bloc de localisation utiliser pour gérer les requêtes.

Syntaxe du Bloc de Localisation

Avant d’aborder la manière dont Nginx décide quel bloc de localisation utiliser pour traiter les requêtes, examinons certaines des syntaxes que vous pourriez voir dans les définitions de blocs de localisation. Les blocs de localisation se trouvent à l’intérieur des blocs serveur (ou d’autres blocs de localisation) et sont utilisés pour décider comment traiter l’URI de la requête (la partie de la requête qui vient après le nom de domaine ou l’adresse IP/port).

Les blocs de localisation ont généralement la forme suivante:

location optional_modifier location_match {

    . . .

}

Le location_match ci-dessus définit ce que Nginx doit vérifier par rapport à l’URI de la requête. L’existence ou la non-existence du modificateur dans l’exemple ci-dessus affecte la manière dont Nginx tente de faire correspondre le bloc de localisation. Les modificateurs ci-dessous feront en sorte que le bloc de localisation associé soit interprété comme suit:

  • (none): Si aucun modificateur n’est présent, la localisation est interprétée comme une correspondance de préfixe. Cela signifie que la localisation donnée sera mise en correspondance avec le début de l’URI de la requête pour déterminer une correspondance.
  • =: Si un signe égal est utilisé, ce bloc sera considéré comme une correspondance si l’URI de la requête correspond exactement à la localisation donnée.
  • ~: Si un modificateur tilde est présent, cet emplacement sera interprété comme une correspondance d’expression régulière sensible à la casse.
  • ~*: Si un modificateur tilde et astérisque est utilisé, le bloc de localisation sera interprété comme une correspondance d’expression régulière insensible à la casse.
  • ^~: Si un modificateur carat et tilde est présent, et si ce bloc est sélectionné comme la meilleure correspondance non basée sur une expression régulière, la correspondance d’expression régulière ne se produira pas.

Exemples de syntaxe de blocs de localisation

À titre d’exemple de correspondance de préfixe, le bloc de localisation suivant peut être sélectionné pour répondre aux URI de requête qui ressemblent à /site, /site/page1/index.html, ou /site/index.html:

location /site {

    . . .

}

Pour une démonstration de la correspondance exacte de l’URI de requête, ce bloc sera toujours utilisé pour répondre à un URI de requête qui ressemble à /page1. Il ne sera pas utilisé pour répondre à un URI de requête /page1/index.html. Gardez à l’esprit que si ce bloc est sélectionné et que la requête est remplie en utilisant une page d’index, une redirection interne aura lieu vers un autre emplacement qui sera le gestionnaire réel de la requête :

location = /page1 {

    . . .

}

À titre d’exemple d’un emplacement devant être interprété comme une expression régulière sensible à la casse, ce bloc pourrait être utilisé pour traiter les demandes pour /tortoise.jpg, mais pas pour /FLOWER.PNG :

location ~ \.(jpe?g|png|gif|ico)$ {

    . . .

}

A block that would allow for case-insensitive matching similar to the above is shown below. Here, both /tortoise.jpg and /FLOWER.PNG could be handled by this block:

location ~* \.(jpe?g|png|gif|ico)$ {

    . . .

}

Enfin, ce bloc empêcherait la correspondance d’expressions régulières de se produire s’il est déterminé être le meilleur non-correspondance par expression régulière. Il pourrait traiter les demandes pour /costumes/ninja.html :

location ^~ /costumes {

    . . .

}

Comme vous le voyez, les modificateurs indiquent comment le bloc d’emplacement doit être interprété. Cependant, cela ne nous dit pas l’algorithme que Nginx utilise pour décider quel bloc d’emplacement envoyer la demande. Nous verrons cela ensuite.

Comment Nginx choisit quel emplacement utiliser pour traiter les demandes

Nginx choisit l’emplacement qui sera utilisé pour traiter une demande de manière similaire à la manière dont il sélectionne un bloc de serveurs. Il passe par un processus qui détermine le meilleur bloc d’emplacement pour une demande donnée. Comprendre ce processus est une exigence cruciale pour être capable de configurer Nginx de manière fiable et précise.

En gardant à l’esprit les types de déclarations d’emplacement que nous avons décrits ci-dessus, Nginx évalue les contextes d’emplacement possibles en comparant l’URI de la demande à chacun des emplacements. Il le fait en utilisant l’algorithme suivant :

  • Nginx commence par vérifier toutes les correspondances de localisation basées sur le préfixe (tous les types de localisation ne nécessitant pas d’expression régulière). Il vérifie chaque emplacement par rapport à l’URI de la requête complète.
  • Tout d’abord, Nginx recherche une correspondance exacte. Si un bloc de localisation utilisant le modificateur = est trouvé pour correspondre exactement à l’URI de la requête, ce bloc de localisation est immédiatement sélectionné pour servir la demande.
  • S’il n’y a pas de correspondances exactes (avec le modificateur =), Nginx passe alors à l’évaluation des préfixes non exacts. Il découvre le plus long préfixe correspondant pour l’URI de requête donnée, qu’il évalue ensuite comme suit :
    • Si le plus long préfixe correspondant a le modificateur ^~, alors Nginx mettra immédiatement fin à sa recherche et sélectionnera cet emplacement pour servir la requête.
    • Si le plus long préfixe correspondant n’utilise pas le modificateur ^~, la correspondance est stockée par Nginx pour le moment afin que l’attention de la recherche puisse être déplacée.
  • Après que l’emplacement du préfixe le plus long a été déterminé et stocké, Nginx passe à l’évaluation des emplacements d’expressions régulières (à la fois sensibles à la casse et insensibles). S’il y a des emplacements d’expressions régulières à l’intérieur de l’emplacement du préfixe le plus long, Nginx les déplacera en haut de sa liste d’emplacements d’expressions régulières à vérifier. Ensuite, Nginx essaie de faire correspondre séquentiellement aux emplacements d’expressions régulières. Le premier emplacement d’expression régulière qui correspond à l’URI de la demande est immédiatement sélectionné pour servir la demande.
  • Si aucun emplacement d’expressions régulières ne correspond à l’URI de la demande, l’emplacement de préfixe précédemment stocké est sélectionné pour servir la demande.

Il est important de comprendre que, par défaut, Nginx servira les correspondances d’expressions régulières de préférence aux correspondances de préfixes. Cependant, il évalue d’abord les emplacements de préfixes, permettant à l’administrateur de remplacer cette tendance en spécifiant des emplacements en utilisant les modificateurs = et ^~.

Il est également important de noter que, tandis que les emplacements de préfixes sélectionnent généralement en fonction du plus long et du plus spécifique, l’évaluation des expressions régulières s’arrête lorsque le premier emplacement correspondant est trouvé. Cela signifie que la position dans la configuration a de vastes implications pour les emplacements d’expressions régulières.

Enfin, il est important de comprendre que les correspondances d’expressions régulières à l’intérieur de la correspondance de préfixe la plus longue « sautera la ligne » lorsque Nginx évaluera les emplacements regex. Ceux-ci seront évalués, dans l’ordre, avant que toutes les autres correspondances d’expressions régulières ne soient considérées. Maxim Dounin, un développeur Nginx incroyablement utile, explique dans cet article cette partie de l’algorithme de sélection.

Quand l’évaluation du bloc de localisation saute-t-elle vers d’autres emplacements?

En général, lorsque qu’un bloc de localisation est sélectionné pour répondre à une requête, la requête est entièrement traitée dans ce contexte à partir de ce point. Seul le bloc de localisation sélectionné et les directives héritées déterminent comment la requête est traitée, sans interférence des blocs de localisation frères.

Bien que cela soit une règle générale qui vous permettra de concevoir vos blocs de localisation de manière prévisible, il est important de réaliser qu’il existe des moments où une nouvelle recherche de localisation est déclenchée par certaines directives dans le bloc de localisation sélectionné. Les exceptions à la règle « un seul bloc de localisation » peuvent avoir des implications sur la manière dont la requête est effectivement traitée et peuvent ne pas correspondre aux attentes que vous aviez lors de la conception de vos blocs de localisation.

Certaines directives qui peuvent entraîner ce type de redirection interne sont :

  • index
  • try_files
  • rewrite
  • error_page

Examinons-les brièvement.

La directive index conduit toujours à une redirection interne si elle est utilisée pour traiter la demande. Les correspondances d’emplacement exact sont souvent utilisées pour accélérer le processus de sélection en mettant fin immédiatement à l’exécution de l’algorithme. Cependant, si vous effectuez une correspondance d’emplacement exact qui est un répertoire, il y a de fortes chances que la demande soit redirigée vers un autre emplacement pour le traitement réel.

Dans cet exemple, le premier emplacement est associé à une URI de demande /exact, mais pour traiter la demande, la directive index héritée par le bloc initie une redirection interne vers le deuxième bloc:

index index.html;

location = /exact {

    . . .

}

location / {

    . . .

}

Dans le cas ci-dessus, si vous avez vraiment besoin que l’exécution reste dans le premier bloc, vous devrez trouver une méthode différente pour satisfaire la demande vers le répertoire. Par exemple, vous pourriez définir un index invalide pour ce bloc et activer autoindex:

location = /exact {
    index nothing_will_match;
    autoindex on;
}

location  / {

    . . .

}

C’est une façon d’empêcher un index de changer de contexte, mais cela n’est probablement pas utile pour la plupart des configurations. Principalement, une correspondance exacte sur les répertoires peut être utile pour des choses comme la réécriture de la demande (ce qui entraîne également une nouvelle recherche de localisation).

Un autre cas où l’emplacement de traitement peut être réévalué est avec la directive try_files. Cette directive dit à Nginx de vérifier l’existence d’un ensemble nommé de fichiers ou de répertoires. Le dernier paramètre peut être une URI vers laquelle Nginx effectuera une redirection interne.

Considérez la configuration suivante :

root /var/www/main;
location / {
    try_files $uri $uri.html $uri/ /fallback/index.html;
}

location /fallback {
    root /var/www/another;
}

Dans l’exemple ci-dessus, si une requête est faite pour /blahblah, la première localisation obtiendra initialement la requête. Elle tentera de trouver un fichier appelé blahblah dans le répertoire /var/www/main. Si elle ne peut pas en trouver un, elle cherchera ensuite un fichier appelé blahblah.html. Elle vérifiera alors s’il existe un répertoire appelé blahblah/ dans le répertoire /var/www/main. En cas d’échec de toutes ces tentatives, elle redirigera vers /fallback/index.html. Cela déclenchera une autre recherche de localisation qui sera capturée par le deuxième bloc de localisation. Cela servira le fichier /var/www/another/fallback/index.html.

Une autre directive qui peut entraîner un transfert de bloc de localisation est la directive rewrite. Lors de l’utilisation du paramètre last avec la directive rewrite, ou lors de l’utilisation d’aucun paramètre du tout, Nginx recherchera une nouvelle localisation correspondante en fonction des résultats de la réécriture.

Par exemple, si nous modifions le dernier exemple pour inclure une réécriture, nous pouvons voir que la requête est parfois transmise directement au deuxième bloc de localisation sans se fier à la directive try_files:

root /var/www/main;
location / {
    rewrite ^/rewriteme/(.*)$ /$1 last;
    try_files $uri $uri.html $uri/ /fallback/index.html;
}

location /fallback {
    root /var/www/another;
}

Dans l’exemple ci-dessus, une demande pour /rewriteme/hello sera initialement traitée par le premier bloc de localisation. Elle sera réécrite en /hello et une localisation sera recherchée. Dans ce cas, elle correspondra à nouveau à la première localisation et sera traitée par le try_files comme d’habitude, peut-être renvoyée à /fallback/index.html si rien n’est trouvé (en utilisant la redirection interne try_files que nous avons discutée ci-dessus).

Cependant, si une requête est faite pour /rewriteme/fallback/hello, le premier bloc sera à nouveau sélectionné. La réécriture sera appliquée à nouveau, cette fois-ci résultant en /fallback/hello. La requête sera alors traitée dans le deuxième bloc de localisation.

A related situation happens with the return directive when sending the 301 or 302 status codes. The difference in this case is that it results in an entirely new request in the form of an externally visible redirect. This same situation can occur with the rewrite directive when using the redirect or permanent flags. However, these location searches shouldn’t be unexpected, since externally visible redirects always result in a new request.

La directive error_page peut entraîner une redirection interne similaire à celle créée par try_files. Cette directive est utilisée pour définir ce qui doit se passer lorsque certains codes d’état sont rencontrés. Cela ne sera probablement jamais exécuté si try_files est défini, car cette directive gère tout le cycle de vie d’une requête.

Considérez cet exemple:

root /var/www/main;

location / {
    error_page 404 /another/whoops.html;
}

location /another {
    root /var/www;
}

Chaque requête (sauf celles commençant par /another) sera gérée par le premier bloc, qui servira des fichiers depuis /var/www/main. Cependant, si un fichier n’est pas trouvé (un statut 404), une redirection interne vers /another/whoops.html se produira, entraînant une nouvelle recherche de localisation qui aboutira finalement sur le deuxième bloc. Ce fichier sera servi depuis /var/www/another/whoops.html.

Comme vous pouvez le voir, comprendre les circonstances dans lesquelles Nginx déclenche une nouvelle recherche de localisation peut aider à prédire le comportement que vous verrez lors de la réalisation de requêtes.

Conclusion

Comprendre les façons dont Nginx traite les demandes des clients peut grandement faciliter votre travail en tant qu’administrateur. Vous serez en mesure de savoir quel bloc serveur Nginx sélectionnera en fonction de chaque demande client. Vous pourrez également déterminer comment le bloc de localisation sera sélectionné en fonction de l’URI de la demande. Dans l’ensemble, connaître la manière dont Nginx sélectionne différents blocs vous donnera la capacité de retracer les contextes que Nginx appliquera pour servir chaque demande.

Source:
https://www.digitalocean.com/community/tutorials/understanding-nginx-server-and-location-block-selection-algorithms