Comprensión de los algoritmos de selección de servidor y bloque de ubicación de Nginx

Introducción

Nginx es uno de los servidores web más populares del mundo. Puede manejar cargas altas con muchas conexiones de clientes concurrentes y puede funcionar como servidor web, servidor de correo o servidor proxy inverso.

En esta guía, discutiremos algunos de los detalles detrás de escena que determinan cómo Nginx procesa las solicitudes de clientes. Comprender estas ideas puede ayudar a eliminar las conjeturas al diseñar bloques de servidor y de ubicación, y puede hacer que el manejo de solicitudes parezca menos impredecible.

Configuraciones de Bloques Nginx

Nginx divide lógicamente las configuraciones destinadas a servir diferentes contenidos en bloques, que viven en una estructura jerárquica. Cada vez que se realiza una solicitud de cliente, Nginx inicia un proceso para determinar qué bloques de configuración deben utilizarse para manejar la solicitud. Este proceso de decisión es lo que discutiremos en esta guía.

Los bloques principales que discutiremos son el bloque server y el bloque location.

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.

Cómo decide Nginx qué bloque de servidor manejará una solicitud

Como Nginx permite al administrador definir múltiples bloques de servidor que funcionan como instancias separadas de servidores web virtuales, necesita un procedimiento para determinar cuál de estos bloques de servidor se utilizará para satisfacer una solicitud.

Lo hace a través de un sistema definido de comprobaciones que se utilizan para encontrar la mejor coincidencia posible. Los principales directivas de bloques de servidor con los que se preocupa Nginx durante este proceso son la directiva listen y la directiva server_name.

Análisis de la directiva listen para encontrar posibles coincidencias

Primero, Nginx examina la dirección IP y el puerto de la solicitud. Lo compara con la directiva listen de cada servidor para construir una lista de los bloques de servidor que posiblemente puedan resolver la solicitud.

La directiva listen suele definir a qué dirección IP y puerto responderá el bloque del servidor. Por defecto, cualquier bloque de servidor que no incluya una directiva listen se le asignan los parámetros de escucha 0.0.0.0:80 (o 0.0.0.0:8080 si Nginx está siendo ejecutado por un usuario normal, no root). Esto permite que estos bloques respondan a solicitudes en cualquier interfaz en el puerto 80, pero este valor predeterminado no tiene mucho peso en el proceso de selección del servidor.

La directiva listen se puede establecer como:

  • Una combinación de dirección IP/puerto.
  • 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.
  • La ruta a un socket Unix.

La última opción generalmente solo tendrá implicaciones al pasar solicitudes entre diferentes servidores.

Al intentar determinar a qué bloque de servidor enviar una solicitud, Nginx primero intentará decidir basándose en la especificidad de la directiva listen utilizando las siguientes reglas:

  • Nginx traduce todas las directivas listen “incompletas” sustituyendo los valores faltantes con sus valores predeterminados para que cada bloque pueda evaluarse por su dirección IP y puerto. Algunos ejemplos de estas traducciones son:
    • Un bloque sin directiva listen usa el valor 0.0.0.0:80.
    • Un bloque establecido en una dirección IP 111.111.111.111 sin puerto se convierte en 111.111.111.111:80
    • Un bloque establecido en el puerto 8888 sin dirección IP se convierte en 0.0.0.0:8888
  • Nginx luego intenta recopilar una lista de los bloques del servidor que coincidan más específicamente con la solicitud en función de la dirección IP y el puerto. Esto significa que cualquier bloque que funcionalmente esté utilizando 0.0.0.0 como su dirección IP (para coincidir con cualquier interfaz), no será seleccionado si hay bloques coincidentes que enumeren una dirección IP específica. En cualquier caso, el puerto debe coincidir exactamente.
  • Si solo hay una coincidencia más específica, ese bloque de servidor se utilizará para atender la solicitud. Si hay varios bloques de servidor con el mismo nivel de especificidad coincidente, entonces Nginx comienza a evaluar la directiva server_name de cada bloque de servidor.

Es importante entender que Nginx solo evaluará la directiva server_name cuando necesite distinguir entre bloques de servidor que coincidan al mismo nivel de especificidad en la directiva listen. Por ejemplo, si example.com está alojado en el puerto 80 de 192.168.1.10, una solicitud para example.com siempre será atendida por el primer bloque en este ejemplo, a pesar de la directiva server_name en el segundo bloque.

server {
    listen 192.168.1.10;

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

En caso de que más de un bloque de servidor coincida con igual especificidad, el siguiente paso es verificar la directiva server_name.

Análisis de la directiva server_name para elegir una coincidencia

A continuación, para evaluar aún más las solicitudes que tienen directivas listen igualmente específicas, Nginx verifica el encabezado Host de la solicitud. Este valor contiene el dominio o la dirección IP a la que realmente estaba intentando acceder el cliente.

Nginx intenta encontrar la mejor coincidencia para el valor que encuentra al examinar la directiva server_name dentro de cada uno de los bloques de servidor que aún son candidatos de selección. Nginx evalúa estos utilizando la siguiente fórmula:

  • Nginx primero intentará encontrar un bloque de servidor con un server_name que coincida con el valor en el encabezado Host de la solicitud exactamente. Si se encuentra, el bloque asociado se utilizará para atender la solicitud. Si se encuentran múltiples coincidencias exactas, se utilizará la primera.
  • Si no se encuentra ninguna coincidencia exacta, Nginx luego intentará encontrar un bloque de servidor con un server_name que coincida utilizando un comodín de inicio (indicado por un * al principio del nombre en la configuración). Si se encuentra uno, ese bloque se utilizará para atender la solicitud. Si se encuentran múltiples coincidencias, se utilizará la más larga para atender la solicitud.
  • Si no se encuentra ninguna coincidencia utilizando un comodín de inicio, Nginx buscará un bloque de servidor con un server_name que coincida utilizando un comodín de final (indicado por un nombre de servidor que termina con un * en la configuración). Si se encuentra uno, ese bloque se utiliza para atender la solicitud. Si se encuentran múltiples coincidencias, se utilizará la más larga para atender la solicitud.
  • Si no se encuentra ninguna coincidencia utilizando un comodín de coincidencia al final, Nginx luego evalúa los bloques del servidor que definen el server_name utilizando expresiones regulares (indicadas por un ~ antes del nombre). El primer server_name con una expresión regular que coincida con el encabezado “Host” se utilizará para servir la solicitud.
  • Si no se encuentra ninguna coincidencia con expresiones regulares, Nginx luego selecciona el bloque del servidor predeterminado para esa dirección IP y puerto.

Cada combinación de dirección IP/puerto tiene un bloque de servidor predeterminado que se utilizará cuando no se pueda determinar un curso de acción con los métodos anteriores. Para una combinación de dirección IP/puerto, este será el primer bloque en la configuración o el bloque que contiene la opción default_server como parte de la directiva listen (lo que anularía el algoritmo de primer encontrado). Solo puede haber una declaración de default_server por cada combinación de dirección IP/puerto.

Ejemplos

Si hay un server_name definido que coincida exactamente con el valor del encabezado Host, se selecciona ese bloque del servidor para procesar la solicitud.

En este ejemplo, si el encabezado Host de la solicitud se estableció en host1.example.com, se seleccionaría el segundo servidor:

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

    . . .

}

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

    . . .

}

Si no se encuentra una coincidencia exacta, Nginx luego verifica si hay un server_name con un comodín inicial que coincida. La coincidencia más larga que comience con un comodín será seleccionada para cumplir con la solicitud.

En este ejemplo, si la solicitud tuviera un encabezado Host de www.example.org, se seleccionaría el segundo bloque del servidor:

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

    . . .

}

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

    . . .

}

server {
    listen 80;
    server_name *.org;

    . . .

}

Si no se encuentra una coincidencia con un comodín inicial, Nginx luego verificará si existe una coincidencia utilizando un comodín al final de la expresión. En este punto, se seleccionará la coincidencia más larga que termine con un comodín para atender la solicitud.

Por ejemplo, si la solicitud tiene un encabezado Host establecido en www.example.com, se seleccionará el tercer bloque del servidor:

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

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

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

    . . .

}

Si no se pueden encontrar coincidencias de comodines, entonces Nginx pasará a intentar hacer coincidir las directivas server_name que usan expresiones regulares. La primera expresión regular que coincida será seleccionada para responder a la solicitud.

Por ejemplo, si el encabezado Host de la solicitud está configurado como www.example.com, entonces se seleccionará el segundo bloque del servidor para satisfacer la solicitud:

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 ninguno de los pasos anteriores puede satisfacer la solicitud, entonces la solicitud se pasará al servidor predeterminado para la dirección IP y puerto coincidentes.

Bloques de ubicación coincidentes

Similar al proceso que Nginx utiliza para seleccionar el bloque del servidor que procesará una solicitud, Nginx también tiene un algoritmo establecido para decidir qué bloque de ubicación dentro del servidor utilizar para manejar las solicitudes.

Sintaxis del Bloque de Ubicación

Antes de cubrir cómo Nginx decide qué bloque de ubicación utilizar para manejar las solicitudes, repasemos parte de la sintaxis que podría ver en las definiciones de bloque de ubicación. Los bloques de ubicación residen dentro de bloques de servidor (u otros bloques de ubicación) y se utilizan para decidir cómo procesar el URI de la solicitud (la parte de la solicitud que viene después del nombre de dominio o la dirección IP/puerto).

Los bloques de ubicación generalmente tienen la siguiente forma:

location optional_modifier location_match {

    . . .

}

El location_match en el ejemplo anterior define contra qué debería verificar Nginx el URI de la solicitud. La existencia o no existencia del modificador en el ejemplo anterior afecta la forma en que Nginx intenta hacer coincidir el bloque de ubicación. Los modificadores a continuación harán que el bloque de ubicación asociado se interprete de la siguiente manera:

  • (ninguno): Si no hay modificadores presentes, la ubicación se interpreta como una coincidencia de prefijo. Esto significa que la ubicación dada se comparará con el inicio del URI de la solicitud para determinar una coincidencia.
  • =: Si se utiliza un signo igual, este bloque se considerará una coincidencia si el URI de la solicitud coincide exactamente con la ubicación dada.
  • ~: Si hay un modificador de tilde presente, esta ubicación se interpretará como una coincidencia de expresión regular sensible a mayúsculas y minúsculas.
  • ~*: Si se usa un modificador de tilde y asterisco, el bloque de ubicación se interpretará como una coincidencia de expresión regular sin distinguir entre mayúsculas y minúsculas.
  • ^~: Si está presente un modificador de circunflejo y tilde, y si este bloque es seleccionado como la mejor coincidencia no basada en expresiones regulares, no se llevará a cabo la coincidencia de expresión regular.

Ejemplos que demuestran la sintaxis del bloque de ubicación

Como ejemplo de coincidencia de prefijo, el siguiente bloque de ubicación puede ser seleccionado para responder a solicitudes de URIs que se parecen a /site, /site/page1/index.html, o /site/index.html:

location /site {

    . . .

}

Para una demostración de coincidencia exacta de URI de solicitud, este bloque siempre se utilizará para responder a un URI de solicitud que se parezca a /page1. No se utilizará para responder a un URI de solicitud /page1/index.html. Tenga en cuenta que si este bloque es seleccionado y se cumple la solicitud usando una página de índice, se llevará a cabo una redirección interna a otra ubicación que será el manejador real de la solicitud:

location = /page1 {

    . . .

}

Como ejemplo de una ubicación que debe interpretarse como una expresión regular sensible a mayúsculas y minúsculas, este bloque podría usarse para manejar solicitudes para /tortoise.jpg, pero no para /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)$ {

    . . .

}

Finalmente, este bloque evitaría que ocurra la coincidencia de expresiones regulares si se determina que es la mejor coincidencia no basada en expresiones regulares. Podría manejar solicitudes para /costumes/ninja.html:

location ^~ /costumes {

    . . .

}

Como ves, los modificadores indican cómo debe interpretarse el bloque de ubicación. Sin embargo, esto no nos dice el algoritmo que Nginx utiliza para decidir a qué bloque de ubicación enviar la solicitud. Lo veremos a continuación.

Cómo elige Nginx qué ubicación usar para manejar solicitudes

Nginx elige la ubicación que se utilizará para servir una solicitud de manera similar a cómo selecciona un bloque de servidor. Realiza un proceso que determina el mejor bloque de ubicación para cualquier solicitud dada. Comprender este proceso es un requisito crucial para poder configurar Nginx de manera confiable y precisa.

Teniendo en cuenta los tipos de declaraciones de ubicación que describimos anteriormente, Nginx evalúa los posibles contextos de ubicación comparando el URI de la solicitud con cada una de las ubicaciones. Hace esto utilizando el siguiente algoritmo:

  • Nginx comienza revisando todas las coincidencias de ubicación basadas en prefijos (todos los tipos de ubicación que no involucran una expresión regular). Verifica cada ubicación contra el URI de solicitud completo.
  • Primero, Nginx busca una coincidencia exacta. Si se encuentra un bloque de ubicación que utiliza el modificador = para que coincida exactamente con el URI de solicitud, este bloque de ubicación se selecciona inmediatamente para servir la solicitud.
  • Si no se encuentran coincidencias exactas (con el modificador =), Nginx luego procede a evaluar los prefijos no exactos. Descubre la ubicación de prefijo coincidente más larga para el URI de solicitud dado, que luego evalúa de la siguiente manera:
    • Si la ubicación de prefijo coincidente más larga tiene el modificador ^~, entonces Nginx terminará inmediatamente su búsqueda y seleccionará esta ubicación para servir la solicitud.
    • Si la ubicación de prefijo coincidente más larga no utiliza el modificador ^~, la coincidencia se almacena temporalmente por Nginx para que el foco de la búsqueda pueda cambiar.
  • Después de que se determina y almacena la ubicación del prefijo de coincidencia más larga, Nginx procede a evaluar las ubicaciones de expresiones regulares (tanto sensibles como insensibles a mayúsculas y minúsculas). Si hay alguna ubicación de expresión regular dentro de la ubicación del prefijo de coincidencia más larga, Nginx moverá esas al principio de su lista de ubicaciones de expresiones regulares para verificar. Luego, Nginx intenta hacer coincidir contra las ubicaciones de expresiones regulares secuencialmente. La primera ubicación de expresión regular que coincida con la URI de la solicitud se selecciona inmediatamente para atender la solicitud.
  • Si no se encuentran ubicaciones de expresiones regulares que coincidan con la URI de la solicitud, se selecciona la ubicación de prefijo previamente almacenada para atender la solicitud.

Es importante entender que, de forma predeterminada, Nginx servirá coincidencias de expresiones regulares en preferencia a coincidencias de prefijos. Sin embargo, evalúa primero las ubicaciones de prefijo, lo que permite al administrador anular esta tendencia especificando ubicaciones mediante los modificadores = y ^~.

También es importante tener en cuenta que, si bien las ubicaciones de prefijo generalmente se seleccionan en función de la coincidencia más larga y específica, la evaluación de expresiones regulares se detiene cuando se encuentra la primera ubicación coincidente. Esto significa que la posición dentro de la configuración tiene amplias implicaciones para las ubicaciones de expresiones regulares.

Finalmente, es importante entender que las coincidencias de expresiones regulares dentro del emparejamiento del prefijo más largo “saltarán la línea” cuando Nginx evalúe las ubicaciones de las expresiones regulares. Estas serán evaluadas, en orden, antes de que se consideren cualquier otra coincidencia de expresión regular. Maxim Dounin, un desarrollador de Nginx increíblemente útil, explica en esta publicación esta parte del algoritmo de selección.

¿Cuándo Salta la Evaluación del Bloque de Ubicación a Otras Ubicaciones?

En general, cuando se selecciona un bloque de ubicación para atender una solicitud, la solicitud se maneja completamente dentro de ese contexto a partir de ese punto en adelante. Solo la ubicación seleccionada y las directivas heredadas determinan cómo se procesa la solicitud, sin interferencia de bloques de ubicación hermanos.

Aunque esta es una regla general que le permitirá diseñar sus bloques de ubicación de manera predecible, es importante darse cuenta de que hay momentos en los que una nueva búsqueda de ubicación es desencadenada por ciertas directivas dentro de la ubicación seleccionada. Las excepciones a la regla de “solo un bloque de ubicación” pueden tener implicaciones sobre cómo se sirve realmente la solicitud y pueden no coincidir con las expectativas que tenía al diseñar sus bloques de ubicación.

Algunas directivas que pueden llevar a este tipo de redirección interna son:

  • index
  • try_files
  • rewrite
  • error_page

Vamos a repasar esto brevemente.

La directiva index siempre conduce a una redirección interna si se utiliza para manejar la solicitud. Las coincidencias de ubicación exacta suelen usarse para acelerar el proceso de selección al finalizar inmediatamente la ejecución del algoritmo. Sin embargo, si realizas una coincidencia de ubicación exacta que es un directorio, existe una buena posibilidad de que la solicitud se redirija a una ubicación diferente para su procesamiento real.

En este ejemplo, la primera ubicación coincide con una URI de solicitud de /exact, pero para manejar la solicitud, la directiva index heredada por el bloque inicia una redirección interna al segundo bloque:

index index.html;

location = /exact {

    . . .

}

location / {

    . . .

}

En el caso anterior, si realmente necesitas que la ejecución permanezca en el primer bloque, tendrás que idear un método diferente para satisfacer la solicitud al directorio. Por ejemplo, podrías establecer un index inválido para ese bloque y activar autoindex:

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

location  / {

    . . .

}

Esta es una forma de evitar que un index cambie de contexto, pero probablemente no sea útil para la mayoría de las configuraciones. Principalmente, una coincidencia exacta en directorios puede ser útil para cosas como reescribir la solicitud (lo que también resulta en una nueva búsqueda de ubicación).

Otro caso donde la ubicación de procesamiento puede ser reevaluada es con la directiva try_files. Esta directiva indica a Nginx que verifique la existencia de un conjunto de archivos o directorios con nombres específicos. El último parámetro puede ser una URI a la que Nginx hará una redirección interna.

Considera la siguiente configuración:

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

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

En el ejemplo anterior, si se hace una solicitud para /blahblah, inicialmente la primera ubicación recibirá la solicitud. Intentará encontrar un archivo llamado blahblah en el directorio /var/www/main. Si no puede encontrar uno, luego buscará un archivo llamado blahblah.html. Luego intentará ver si hay un directorio llamado blahblah/ dentro del directorio /var/www/main. Si fallan todos estos intentos, se redireccionará a /fallback/index.html. Esto desencadenará otra búsqueda de ubicación que será capturada por el segundo bloque de ubicación. Este servirá el archivo /var/www/another/fallback/index.html.

Otra directiva que puede llevar a la transferencia de bloque de ubicación es la directiva rewrite. Cuando se utiliza el parámetro last con la directiva rewrite, o cuando no se utiliza ningún parámetro, Nginx buscará una nueva ubicación coincidente basada en los resultados de la reescritura.

Por ejemplo, si modificamos el último ejemplo para incluir una reescritura, podemos ver que la solicitud a veces se pasa directamente al segundo bloque de ubicación sin depender de la directiva 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;
}

En el ejemplo anterior, una solicitud para /rewriteme/hello será manejada inicialmente por el primer bloque de ubicación. Se reescribirá como /hello y se buscará una ubicación. En este caso, coincidirá nuevamente con la primera ubicación y será procesado por el try_files como de costumbre, tal vez volviendo a /fallback/index.html si no se encuentra nada (usando la redirección interna try_files que discutimos anteriormente).

Sin embargo, si se realiza una solicitud para /rewriteme/fallback/hello, el primer bloque volverá a coincidir. La reescritura se aplicará nuevamente, esta vez resultando en /fallback/hello. La solicitud luego será atendida desde el segundo bloque de ubicación.

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 directiva error_page puede llevar a una redirección interna similar a la creada por try_files. Esta directiva se utiliza para definir qué debe suceder cuando se encuentran ciertos códigos de estado. Esto probablemente nunca se ejecutará si se establece try_files, ya que esa directiva maneja todo el ciclo de vida de una solicitud.

Tome este ejemplo en consideración:

root /var/www/main;

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

location /another {
    root /var/www;
}

Cada solicitud (excepto aquellas que comiencen con /another) será manejada por el primer bloque, que servirá archivos desde /var/www/main. Sin embargo, si no se encuentra un archivo (un estado 404), se producirá una redirección interna a /another/whoops.html, lo que llevará a una nueva búsqueda de ubicación que eventualmente llegará al segundo bloque. Este archivo se servirá desde /var/www/another/whoops.html.

Como puede ver, entender las circunstancias en las que Nginx activa una nueva búsqueda de ubicación puede ayudar a predecir el comportamiento que verá al realizar solicitudes.

Conclusión

Comprender las formas en que Nginx procesa las solicitudes de los clientes puede facilitar mucho tu trabajo como administrador. Podrás saber qué bloque de servidor seleccionará Nginx según cada solicitud del cliente. También podrás determinar cómo se seleccionará el bloque de ubicación según el URI de la solicitud. En general, conocer la forma en que Nginx selecciona diferentes bloques te dará la capacidad de rastrear los contextos que Nginx aplicará para atender cada solicitud.

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