Compreendendo os 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 cliente 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 as solicitações de cliente. Entender essas ideias pode ajudar a eliminar as conjecturas ao projetar blocos de servidor e de localização e pode tornar o tratamento de solicitações menos imprevisível.

Configurações de Bloco do Nginx

O Nginx divide logicamente as configurações destinadas a servir conteúdo diferente em blocos, que residem em uma estrutura hierárquica. Cada vez que uma solicitação de cliente é feita, o Nginx inicia um processo para determinar quais blocos de configuração devem ser usados para lidar com a solicitação. 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 Solicitação

Já que 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 satisfazer uma solicitação.

Ele faz isso através de um sistema definido de verificações que são usadas para encontrar a melhor correspondência possível. As principais diretivas de blocos de servidor com as quais o Nginx se preocupa durante esse 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 a qual endereço IP e porta o bloco do servidor irá responder. Por padrão, qualquer bloco de servidor que não inclua uma diretiva listen recebe os parâmetros de escuta 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 importâ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 socket 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 de 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
  • Nginx então tenta coletar uma lista dos blocos de servidor que correspondem à solicitação de forma mais específica com base no endereço IP e na 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 à solicitação. 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 do 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 tenham 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, olhando para a diretiva server_name dentro de cada um dos blocos de servidor que ainda são candidatos à seleção. O Nginx avalia esses usando a seguinte fórmula:

  • O Nginx primeiro tentará encontrar um bloco de 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, o primeiro será usado.
  • Se nenhuma correspondência exata for encontrada, o Nginx tentará encontrar um bloco de servidor com um server_name que corresponda usando um curinga à esquerda (indicado por um * no início do nome na configuração). Se um for encontrado, 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 de servidor com um server_name que corresponda usando um curinga à direita (indicado por um nome de servidor terminando com um * na configuração). Se um for encontrado, 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 caractere 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 corresponde ao cabeçalho “Host” será usado para atender à solicitação.
  • Se nenhuma correspondência de expressão regular for encontrada, o Nginx então seleciona 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, este será ou o primeiro bloco na configuração ou o bloco que contém a opção default_server como parte da diretiva listen (o que substituiria o algoritmo de primeira correspondência encontrada). Pode haver apenas uma declaração de 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 do servidor é 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 encontrada nenhuma correspondência exata, o Nginx verifica se há um server_name com um curinga no início que se encaixe. 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 nenhuma correspondência for encontrada com um curinga no início, o Nginx verificará se uma correspondência existe 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 passará então a tentar corresponder diretivas server_name que usem 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 for definido como www.example.com, então o segundo bloco de servidor será selecionado para atender à 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 nenhum dos passos acima for capaz de atender à solicitação, então a solicitação será encaminhada para o servidor padrão para o endereço IP e porta correspondentes.

Correspondência de Blocos de Localização

Similar ao processo que o Nginx utiliza para selecionar o bloco de 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 usar para manipular solicitações.

Sintaxe do Bloco de Localização

Antes de abordarmos como o Nginx decide qual bloco de localização usar para manipular solicitações, vamos revisar algumas das sintaxes que você pode ver nas definições de blocos de localização. Os blocos de localização residem 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 de domínio ou endereço IP/porta).

Os blocos de localização geralmente seguem a seguinte forma:

location optional_modifier location_match {

    . . .

}

O localizacao_correspondente no exemplo 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 corresponder o bloco de localização. Os modificadores abaixo farão com que o bloco de localização associado seja interpretado da seguinte forma:

  • (nenhum): Se nenhum modificador estiver presente, a localização é interpretada como uma correspondência de prefixo. Isso significa que a localização fornecida será correspondida ao 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 de acento circunflexo e til está presente e se este bloco for selecionado como a melhor correspondência não regular de expressão, 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 será sempre usado para responder a uma URI de solicitação que se parece com /page1. Ele 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 case-sensitive, 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ão regular ocorresse se for determinado como a melhor correspondência não baseada em expressão regular. Ele poderia lidar com solicitações para /costumes/ninja.html:

location ^~ /costumes {

    . . .

}

Como você vê, 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 revisar 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 maneira semelhante à forma como seleciona um bloco de servidor. Ele passa por um processo que determina o melhor bloco de localização para qualquer solicitação específica. Compreender este processo é um requisito crucial para ser capaz de configurar o Nginx de forma confiável e precisa.

Tendo em mente 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 ao URI de solicitação completo.
  • Primeiro, o Nginx procura uma correspondência exata. Se um bloco de localização usando o modificador = for encontrado para corresponder exatamente ao URI da solicitação, este bloco de localização é imediatamente selecionado para atender à solicitação.
  • Se não forem encontradas correspondências exatas (com o modificador =), o Nginx passa então a avaliar os prefixos não exatos. Ele descobre o local de prefixo de correspondência mais longo para o URI de solicitação dado, 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 alterado.
  • Após a localização do prefixo correspondente mais longo ser determinada e armazenada, o Nginx passa a avaliar as localizações de expressão regular (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 correspondente mais longo, o Nginx moverá essas para o topo de sua lista de localizações de regex para verificar. O Nginx então 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 não forem encontradas localizações de expressão regular que correspondam à 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á correspondências de expressões regulares em preferência às correspondências de prefixos. No entanto, ele avalia primeiro as localizações de prefixo, permitindo que o administrador substitua 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 do maior prefixo “pularão a linha” quando o Nginx avaliar localizações regex. Estas serão avaliadas, em ordem, antes que quaisquer outras correspondências de expressões regulares sejam consideradas. 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 Localização Pula para Outras Localizações?

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

Embora esta seja uma regra geral que permitirá que você projete seus blocos de localização de maneira previsível, é importante perceber que existem momentos em que uma nova busca de localização é acionada por certas diretivas dentro da localização selecionada. As exceções à regra “apenas um bloco de localização” podem ter implicações sobre como a solicitação é realmente atendida e podem não estar alinhadas com as expectativas que você tinha ao projetar seus blocos de localização.

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

  • index
  • try_files
  • rewrite
  • error_page

Vamos examinar brevemente esses.

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 exatas 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 uma localização diferente para processamento real.

Neste exemplo, a primeira localização é correspondida por uma URI de solicitação de /exata, 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 encontrar um método diferente de satisfazer a solicitação para o 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 troque 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 a localização de processamento pode ser reavaliada é 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 uma URI para a 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 feita uma solicitação para /blahblah, a primeira localização inicialmente receberá a solicitação. Ela tentará encontrar um arquivo chamado blahblah no diretório /var/www/main. Se não conseguir encontrar um, tentará procurar por um arquivo chamado blahblah.html. Em seguida, tentará verificar se há um diretório chamado blahblah/ dentro do diretório /var/www/main. Se todas essas tentativas falharem, será redirecionado para /fallback/index.html. Isso acionará outra pesquisa de localização 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 não usar nenhum parâmetro, o Nginx buscará uma nova localização correspondente com base nos resultados do rewrite.

Por exemplo, se modificarmos o último exemplo para incluir um rewrite, podemos ver que a solicitação às vezes é passada diretamente para o segundo bloco de localização 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, uma solicitação para /rewriteme/hello será inicialmente tratada pelo primeiro bloco de localização. Será reescrita para /hello e uma localização será pesquisada. Neste caso, ela corresponderá novamente à primeira localização e será processada pelo try_files como de costume, talvez retornando para /fallback/index.html se nada for encontrado (usando o redirecionamento interno do try_files que discutimos anteriormente).

No entanto, se for feito um pedido para /rewriteme/fallback/hello, o primeiro bloco novamente corresponderá. A reescrita será aplicada novamente, resultando desta vez em /fallback/hello. O pedido será então 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 um redirecionamento interno semelhante ao criado 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, pois 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 você pode ver, entender as circunstâncias em que o Nginx dispara uma nova busca de localização pode ajudar a prever o comportamento que você verá ao fazer solicitações.

Conclusão

Entender as maneiras como o Nginx processa as solicitações dos 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 do cliente. Você também será capaz de determinar 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