Compreendendo Algoritmos de Seleção de Servidor e Bloco de Localização do Nginx

Introdução

O Nginx é um dos servidores web mais populares do mundo. Ele pode lidar com cargas elevadas com muitas conexões de clientes simultâneas e pode funcionar como um servidor web, um servidor de e-mail ou um servidor proxy reverso.

Neste guia, discutiremos alguns dos detalhes nos bastidores que determinam como o Nginx processa os pedidos dos clientes. Compreender essas ideias pode ajudar a eliminar as suposições ao projetar blocos de servidor e de localização, tornando o processamento de pedidos menos imprevisível.

Configurações de Blocos do Nginx

O Nginx divide logicamente as configurações destinadas a servir conteúdo diferente em blocos, que vivem em uma estrutura hierárquica. Cada vez que é feito um pedido de cliente, o Nginx inicia um processo para determinar quais blocos de configuração devem ser usados para lidar com o pedido. Este processo de decisão é o que discutiremos neste guia.

Os principais blocos que discutiremos são o bloco server e o bloco 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.

Como o Nginx Decide Qual Bloco de Servidor Irá Lidar com uma Requisição

Como o Nginx permite que o administrador defina vários blocos de servidor que funcionam como instâncias separadas de servidor web virtual, ele precisa de um procedimento para determinar qual desses blocos de servidor será usado para atender a uma solicitação.

Ele faz isso por meio de um sistema definido de verificações que são usadas para encontrar a melhor correspondência possível. As principais diretivas de bloco de servidor com as quais o Nginx se preocupa durante este processo são a diretiva listen e a diretiva server_name.

Analisando a Diretiva listen para Encontrar Possíveis Correspondências

Primeiro, o Nginx analisa o endereço IP e a porta da solicitação. Ele compara isso com a diretiva listen de cada servidor para construir uma lista dos blocos de servidor que podem resolver possivelmente a solicitação.

A diretiva listen geralmente define em qual endereço IP e porta o bloco do servidor irá responder. Por padrão, qualquer bloco do servidor que não inclua uma diretiva listen recebe os parâmetros de escuta de 0.0.0.0:80 (ou 0.0.0.0:8080 se o Nginx estiver sendo executado por um usuário normal, não root). Isso permite que esses blocos respondam a solicitações em qualquer interface na porta 80, mas esse valor padrão não tem muita relevância no processo de seleção do servidor.

A diretiva listen pode ser definida como:

  • Um combo de endereço IP/porta.
  • 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.
  • O caminho para um soquete Unix.

A última opção geralmente só terá implicações ao passar solicitações entre diferentes servidores.

Ao tentar determinar a qual bloco de servidor enviar uma solicitação, o Nginx primeiro tentará decidir com base na especificidade da diretiva listen usando as seguintes regras:

  • O Nginx traduz todas as diretivas listen “incompletas” substituindo os valores ausentes pelos seus valores padrão, para que cada bloco possa ser avaliado pelo seu endereço IP e porta. Alguns exemplos dessas traduções são:
    • Um bloco sem diretiva listen usa o valor 0.0.0.0:80.
    • Um bloco definido para um endereço IP 111.111.111.111 sem porta torna-se 111.111.111.111:80
    • Um bloco definido para a porta 8888 sem endereço IP torna-se 0.0.0.0:8888
  • O Nginx tenta então recolher uma lista dos blocos de servidor que correspondem mais especificamente ao pedido com base no endereço IP e porta. Isso significa que qualquer bloco que esteja funcionalmente usando 0.0.0.0 como seu endereço IP (para corresponder a qualquer interface) não será selecionado se houver blocos correspondentes que listem um endereço IP específico. Em qualquer caso, a porta deve corresponder exatamente.
  • Se houver apenas uma correspondência mais específica, esse bloco de servidor será usado para atender ao pedido. Se houver vários blocos de servidor com o mesmo nível de especificidade correspondente, o Nginx então começa a avaliar a diretiva server_name de cada bloco de servidor.

É importante entender que o Nginx só avaliará a diretiva server_name quando precisar distinguir entre blocos de servidor que correspondem ao mesmo nível de especificidade na diretiva listen. Por exemplo, se example.com estiver hospedado na porta 80 de 192.168.1.10, uma solicitação para example.com sempre será atendida pelo primeiro bloco neste exemplo, apesar da diretiva server_name no segundo bloco.

server {
    listen 192.168.1.10;

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

No caso de mais de um bloco de servidor corresponder com igual especificidade, o próximo passo é verificar a diretiva server_name.

Analisando a Diretiva server_name para Escolher uma Correspondência

Em seguida, para avaliar ainda mais solicitações que possuem diretivas listen igualmente específicas, o Nginx verifica o cabeçalho Host da solicitação. Este valor contém o domínio ou endereço IP que o cliente estava realmente tentando acessar.

O Nginx tenta encontrar a melhor correspondência para o valor encontrado, examinando a diretiva server_name dentro de cada um dos blocos do servidor que ainda são candidatos à seleção. O Nginx avalia esses usando a seguinte fórmula:

  • O Nginx primeiro tentará encontrar um bloco do servidor com um server_name que corresponda exatamente ao valor no cabeçalho Host da solicitação exatamente. Se isso for encontrado, o bloco associado será usado para atender à solicitação. Se várias correspondências exatas forem encontradas, a primeira será usada.
  • Se nenhuma correspondência exata for encontrada, o Nginx então tentará encontrar um bloco do servidor com um server_name que corresponda usando um curinga à esquerda (indicado por um * no início do nome na configuração). Se encontrar um, esse bloco será usado para atender à solicitação. Se várias correspondências forem encontradas, a mais longa será usada para atender à solicitação.
  • Se nenhuma correspondência for encontrada usando um curinga à esquerda, o Nginx então procura um bloco do servidor com um server_name que corresponda usando um curinga à direita (indicado por um nome de servidor terminando com um * na configuração). Se encontrar um, esse bloco será usado para atender à solicitação. Se várias correspondências forem encontradas, a mais longa será usada para atender à solicitação.
  • Se não for encontrada nenhuma correspondência usando um curinga de final, o Nginx então avalia os blocos do servidor que definem o server_name usando expressões regulares (indicadas por um ~ antes do nome). O primeiro server_name com uma expressão regular que corresponda ao cabeçalho “Host” será usado para atender à solicitação.
  • Se nenhuma correspondência com expressão regular for encontrada, o Nginx seleciona então o bloco do servidor padrão para esse endereço IP e porta.

Cada combinação de endereço IP/porta tem um bloco de servidor padrão que será usado quando uma ação não puder ser determinada com os métodos acima. Para uma combinação de endereço IP/porta, isso será o primeiro bloco na configuração ou o bloco que contém a opção default_server como parte da diretiva listen (o que sobrescreveria o algoritmo de primeira ocorrência). Pode haver apenas uma declaração default_server por cada combinação de endereço IP/porta.

Exemplos

Se houver um server_name definido que corresponda exatamente ao valor do cabeçalho Host, esse bloco de servidor será selecionado para processar a solicitação.

Neste exemplo, se o cabeçalho Host da solicitação estiver definido como host1.example.com, o segundo servidor seria selecionado:

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

    . . .

}

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

    . . .

}

Se não for encontrado um correspondência exata, o Nginx verifica se há um server_name com um curinga de início que se encaixa. A correspondência mais longa começando com um curinga será selecionada para atender à solicitação.

Neste exemplo, se a solicitação tiver um cabeçalho Host de www.example.org, o segundo bloco de servidor será selecionado:

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

    . . .

}

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

    . . .

}

server {
    listen 80;
    server_name *.org;

    . . .

}

Se não for encontrada correspondência com um curinga de início, o Nginx verificará se há uma correspondência usando um curinga no final da expressão. Neste ponto, a correspondência mais longa terminando com um curinga será selecionada para atender à solicitação.

Por exemplo, se a solicitação tiver um cabeçalho Host definido como www.example.com, o terceiro bloco de servidor será selecionado:

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

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

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

    . . .

}

Se nenhuma correspondência de curinga puder ser encontrada, o Nginx então tentará corresponder às diretivas server_name que usam expressões regulares. A primeira expressão regular correspondente será selecionada para responder à solicitação.

Por exemplo, se o cabeçalho Host da solicitação estiver definido como www.example.com, então o segundo bloco de servidor será selecionado para satisfazer a solicitação:

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$;

    . . .

}

Se nenhuma das etapas acima for capaz de satisfazer a solicitação, então a solicitação será passada para o servidor padrão para o endereço IP e porta correspondentes.

Blocos de Localização Correspondentes

Assim como o processo que o Nginx usa para selecionar o bloco do servidor que irá processar uma solicitação, o Nginx também possui um algoritmo estabelecido para decidir qual bloco de localização dentro do servidor será usado para lidar com as solicitações.

Sintaxe do Bloco de Localização

Antes de abordarmos como o Nginx decide qual bloco de localização usar para lidar com as solicitações, vamos revisar alguma sintaxe que você pode ver nas definições de bloco de localização. Os blocos de localização estão dentro de blocos de servidor (ou outros blocos de localização) e são usados para decidir como processar o URI da solicitação (a parte da solicitação que vem após o nome do domínio ou endereço IP/porta).

Os blocos de localização geralmente têm a seguinte forma:

location optional_modifier location_match {

    . . .

}

O localização_correspondente acima define o que o Nginx deve verificar no URI da solicitação. A existência ou não do modificador no exemplo acima afeta a maneira como o Nginx tenta fazer a correspondência do bloco de localização. Os modificadores abaixo farão com que o bloco de localização associado seja interpretado da seguinte maneira:

  • (nenhum): Se nenhum modificador estiver presente, a localização é interpretada como uma correspondência de prefixo. Isso significa que a localização fornecida será comparada com o início do URI da solicitação para determinar uma correspondência.
  • =: Se um sinal de igual for usado, este bloco será considerado uma correspondência se o URI da solicitação corresponder exatamente à localização fornecida.
  • ~: Se um modificador til está presente, esta localização será interpretada como uma correspondência de expressão regular sensível a maiúsculas e minúsculas.
  • ~*: Se um modificador til e asterisco é usado, o bloco de localização será interpretado como uma correspondência de expressão regular insensível a maiúsculas e minúsculas.
  • ^~: Se um modificador circunflexo e til está presente, e se este bloco for selecionado como a melhor correspondência não baseada em expressão regular, a correspondência de expressão regular não ocorrerá.

Exemplos Demonstrando a Sintaxe de Bloco de Localização

Como exemplo de correspondência de prefixo, o seguinte bloco de localização pode ser selecionado para responder a URIs de solicitação que se parecem com /site, /site/page1/index.html, ou /site/index.html:

location /site {

    . . .

}

Para uma demonstração de correspondência exata de URI de solicitação, este bloco sempre será usado para responder a uma URI de solicitação que se parece com /page1. Não será usado para responder a uma URI de solicitação /page1/index.html. Tenha em mente que se este bloco for selecionado e a solicitação for atendida usando uma página de índice, um redirecionamento interno ocorrerá para outra localização que será o manipulador real da solicitação:

location = /page1 {

    . . .

}

Como exemplo de uma localização que deve ser interpretada como uma expressão regular sensível a maiúsculas e minúsculas, este bloco poderia ser usado para lidar com solicitações para /tortoise.jpg, mas não 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 bloco impediria que a correspondência de expressões regulares ocorresse se for determinado como a melhor correspondência não baseada em expressões regulares. Ele poderia lidar com solicitações para /costumes/ninja.html:

location ^~ /costumes {

    . . .

}

Como você pode ver, os modificadores indicam como o bloco de localização deve ser interpretado. No entanto, isso não nos diz o algoritmo que o Nginx usa para decidir para qual bloco de localização enviar a solicitação. Vamos explicar isso a seguir.

Como o Nginx Escolhe Qual Localização Usar para Lidar com Solicitações

O Nginx escolhe a localização que será usada para atender a uma solicitação de forma semelhante à seleção de um bloco de servidor. Ele passa por um processo que determina o melhor bloco de localização para qualquer solicitação. Compreender esse processo é um requisito crucial para ser capaz de configurar o Nginx de forma confiável e precisa.

Levando em consideração os tipos de declarações de localização que descrevemos acima, o Nginx avalia os contextos de localização possíveis comparando o URI da solicitação com cada uma das localizações. Ele faz isso usando o seguinte algoritmo:

  • O Nginx começa verificando todas as correspondências de localização baseadas em prefixo (todos os tipos de localização que não envolvem uma expressão regular). Ele verifica cada localização em relação à URI completa da solicitação.
  • Primeiro, o Nginx procura uma correspondência exata. Se um bloco de localização usando o modificador = for encontrado para corresponder exatamente à URI da solicitação, esse bloco de localização é imediatamente selecionado para atender à solicitação.
  • Se nenhuma correspondência exata (com o modificador =) for encontrada, o Nginx passa a avaliar os prefixos não exatos. Ele descobre o local de prefixo de correspondência mais longo para a URI da solicitação fornecida, que então é avaliado da seguinte forma:
    • Se o local de prefixo de correspondência mais longo tiver o modificador ^~, então o Nginx encerrará imediatamente sua busca e selecionará este local para atender à solicitação.
    • Se o local de prefixo de correspondência mais longo não usar o modificador ^~, a correspondência é armazenada pelo Nginx por enquanto para que o foco da pesquisa possa ser deslocado.
  • Depois que a localização do prefixo de correspondência mais longa é determinada e armazenada, o Nginx passa a avaliar as localizações de expressões regulares (tanto sensíveis quanto insensíveis a maiúsculas e minúsculas). Se houver alguma localização de expressão regular dentro da localização do prefixo de correspondência mais longa, o Nginx moverá essas para o topo de sua lista de localizações de regex para verificar. Em seguida, o Nginx tenta corresponder às localizações de expressão regular sequencialmente. A primeira localização de expressão regular que corresponde à URI da solicitação é imediatamente selecionada para atender à solicitação.
  • Se nenhuma localização de expressão regular for encontrada que corresponda à URI da solicitação, a localização de prefixo armazenada anteriormente é selecionada para atender à solicitação.

É importante entender que, por padrão, o Nginx atenderá a correspondências de expressões regulares preferencialmente às correspondências de prefixo. No entanto, ele avalia as localizações de prefixo primeiro, permitindo ao administrador anular essa tendência especificando localizações usando os modificadores = e ^~.

Também é importante observar que, enquanto as localizações de prefixo geralmente são selecionadas com base na correspondência mais longa e específica, a avaliação de expressões regulares é interrompida quando a primeira localização correspondente é encontrada. Isso significa que a posição dentro da configuração tem vastas implicações para as localizações de expressão regular.

Finalmente, é importante entender que correspondências de expressões regulares dentro da correspondência de prefixo mais longa “saltarão a linha” quando o Nginx avaliar locais de regex. Estes serão avaliados, em ordem, antes que qualquer uma das outras correspondências de expressão regular seja considerada. Maxim Dounin, um desenvolvedor Nginx incrivelmente útil, explica em este post esta parte do algoritmo de seleção.

Quando a Avaliação do Bloco de Local Pula para Outros Locais?

Em termos gerais, quando um bloco de local é selecionado para atender a uma solicitação, a solicitação é tratada inteiramente dentro desse contexto a partir desse ponto em diante. Apenas o local selecionado e as diretivas herdadas determinam como a solicitação é processada, sem interferência de blocos de local irmãos.

Embora esta seja uma regra geral que permitirá que você projete seus blocos de local de forma previsível, é importante perceber que há momentos em que uma nova pesquisa de local é acionada por determinadas diretivas dentro do local selecionado. As exceções à regra de “apenas um bloco de local” podem ter implicações sobre como a solicitação é realmente atendida e podem não estar alinhadas com as expectativas que você teve ao projetar seus blocos de local.

Algumas diretivas que podem levar a esse tipo de redirecionamento interno são:

  • índice
  • try_files
  • rewrite
  • error_page

Vamos passar por estes brevemente.

A diretiva index sempre leva a uma redireção interna se for usada para lidar com a solicitação. As correspondências de localização exata são frequentemente usadas para acelerar o processo de seleção, encerrando imediatamente a execução do algoritmo. No entanto, se você fizer uma correspondência de localização exata que seja um diretório, há uma boa chance de que a solicitação seja redirecionada para um local diferente para processamento real.

Neste exemplo, a primeira localização é correspondida por um URI de solicitação de /exato, mas para lidar com a solicitação, a diretiva index herdada pelo bloco inicia uma redireção interna para o segundo bloco:

index index.html;

location = /exact {

    . . .

}

location / {

    . . .

}

No caso acima, se você realmente precisa que a execução permaneça no primeiro bloco, terá que criar um método diferente para satisfazer a solicitação ao diretório. Por exemplo, você poderia definir um index inválido para esse bloco e ativar autoindex:

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

location  / {

    . . .

}

Esta é uma maneira de evitar que um index mude de contexto, mas provavelmente não é útil para a maioria das configurações. Principalmente uma correspondência exata em diretórios pode ser útil para coisas como reescrever a solicitação (o que também resulta em uma nova pesquisa de localização).

Outra instância onde o local de processamento pode ser reavaliado é com a diretiva try_files. Esta diretiva diz ao Nginx para verificar a existência de um conjunto nomeado de arquivos ou diretórios. O último parâmetro pode ser um URI para o qual o Nginx fará uma redireção interna.

Considere a seguinte configuração:

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

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

No exemplo acima, se for feito um pedido para /blahblah, o primeiro local irá inicialmente receber o pedido. Ele tentará encontrar um arquivo chamado blahblah no diretório /var/www/main. Se não conseguir encontrar um, irá procurar por um arquivo chamado blahblah.html. Em seguida, tentará ver se há um diretório chamado blahblah/ dentro do diretório /var/www/main. Se todas essas tentativas falharem, irá redirecionar para /fallback/index.html. Isso acionará outra busca de local que será capturada pelo segundo bloco de localização. Isso servirá o arquivo /var/www/another/fallback/index.html.

Outra diretiva que pode levar à passagem de um bloco de localização é a diretiva rewrite. Ao usar o parâmetro last com a diretiva rewrite, ou ao usar nenhum parâmetro, o Nginx buscará uma nova localização correspondente com base nos resultados da reescrita.

Por exemplo, se modificarmos o último exemplo para incluir uma reescrita, podemos ver que o pedido às vezes é passado diretamente para o segundo local sem depender da diretiva 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;
}

No exemplo acima, um pedido para /rewriteme/hello será inicialmente tratado pelo primeiro bloco de localização. Será reescrito para /hello e uma localização será pesquisada. Neste caso, corresponderá novamente ao primeiro local e será processado pelo try_files como de costume, talvez retornando para /fallback/index.html se nada for encontrado (usando o redirecionamento interno try_files que discutimos acima).

No entanto, se for feito um pedido para /rewriteme/fallback/hello, o primeiro bloco novamente será correspondido. A reescrita será aplicada novamente, resultando desta vez em /fallback/hello. O pedido então será atendido pelo segundo bloco de localização.

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.

A diretiva error_page pode levar a uma redireção interna semelhante à criada por try_files. Esta diretiva é usada para definir o que deve acontecer quando determinados códigos de status são encontrados. Isso provavelmente nunca será executado se try_files estiver configurado, já que essa diretiva manipula todo o ciclo de vida de um pedido.

Considere este exemplo:

root /var/www/main;

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

location /another {
    root /var/www;
}

Todo pedido (exceto aqueles que começam com /another) será tratado pelo primeiro bloco, que servirá arquivos de /var/www/main. No entanto, se um arquivo não for encontrado (um status 404), ocorrerá um redirecionamento interno para /another/whoops.html, levando a uma nova busca de localização que eventualmente chegará ao segundo bloco. Este arquivo será servido de /var/www/another/whoops.html.

Como pode ver, entender as circunstâncias em que o Nginx desencadeia uma nova busca de localização pode ajudar a prever o comportamento que você verá ao fazer pedidos.

Conclusão

Compreender as maneiras como o Nginx processa solicitações de clientes pode facilitar muito o seu trabalho como administrador. Você será capaz de saber qual bloco de servidor o Nginx selecionará com base em cada solicitação de cliente. Você também será capaz de identificar como o bloco de localização será selecionado com base no URI da solicitação. No geral, conhecer a maneira como o Nginx seleciona diferentes blocos lhe dará a capacidade de rastrear os contextos que o Nginx aplicará para atender a cada solicitação.

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