Алгоритмы выбора блоков server и location в Nginx

Nginx – один из популярнейших веб-серверов в мире. Он может обрабатывать высокие нагрузки и большое количество одновременных подключений. Также Nginx можно использовать в качестве балансировщика, почтового сервера или обратного прокси.

Данный мануал расскажет, как Nginx обрабатывает клиентские запросы. Понимание этого механизма поможет вам оптимизировать обработку запросов.

Конфигурация блоков Nginx

Nginx логически делит конфигурации, предназначенные для обслуживания различного контента, на блоки, которые собираются в иерархическую структуру. Обработку каждого запроса клиента Nginx начинает с определения необходимых блоков конфигурации. Этот процесс принятия решений будет центральной темой данного мануала.

Основные блоки, которые мы обсудим, называются server и location.

Блок server – это подмножество конфигурации Nginx, которая определяет виртуальный сервер, используемый для обработки запросов определенного типа. Администраторы часто настраивают несколько блоков server, где каждый блок обрабатывает соединения на основе запрошенного домена, порта и IP-адреса.

Блок location находится в блоке server и используется для того, чтобы Nginx мог обрабатывать запросы для разных ресурсов и URI родительского сервера. С помощью этого блока администратор может разделить пространство URI требуемым образом. Это чрезвычайно гибкая модель.

1: Поиск блока server

Nginx позволяет определять несколько блоков server, которые функционируют как отдельные экземпляры виртуальных веб-серверов. Потому Nginx необходима процедура определения того, какой из этих блоков будет использоваться для обработки запроса.

Для этого Nginx применяет определенную систему проверок, которые используются для поиска наилучшего совпадения. Основные директивы блока server, которые помогают Nginx определить требуемый блок, — это listen и server_name.

Директива listen

Сначала Nginx смотрит на IP-адрес и порт запроса. Он сопоставляет эти значения с директивой listen каждого блока server и создает список блоков, которые могут обслужить запрос.

Директива listen обычно определяет IP-адрес и порт блока server. По умолчанию любой блок server, в котором нет директивы listen, получает параметры 0.0.0.0:80 (или 0.0.0.0:8080, если Nginx запускается обычным пользователем без полномочий root). Это позволяет таким блокам отвечать на запросы на любом интерфейсе по порту 80. Но это стандартное значение не имеет большого веса в процессе выбора блока.

Директива listen может указывать:

  • IP-адрес и порт.
  • Только IP-адрес (тогда будет использоваться порт по умолчанию 80).
  • Только порт (тогда будут прослушиваться все интерфейсы).
  • Путь к Unix-сокету.

Последний вариант, как правило, используется только при передаче запросов между разными серверами.

Сначала Nginx попробует выбрать блок на основе директивы listen, используя следующие правила:

  • Nginx переводит все «неполные» директивы listen, заменяя отсутствующие значения значениями по умолчанию, чтобы затем оценить каждый блок по его IP-адресу и порту. Например:
    • Если в блоке нет директивы listen, блоку будет присвоено значение 0.0.0.0:80.
    • Если в блоке указан только IP-адрес 111.111.111.111, ему будет присвоен стандартный порт: 111.111.111.111:80.
    • Если в блоке указан только порт 8888, ему будет присвоен стандартный IP-адрес: 0.0.0.0:8888.
  • Затем Nginx пытается собрать список блоков server, которые соответствуют запросу, в частности, на основе IP-адреса и порта. Это означает, что любой блок, который использует IP-адрес 0.0.0.0, не будет выбран, если есть блоки, которые настроены на заданный IP-адрес. Порт должен совпадать точно.
  • Если веб-сервер находит всего одно совпадение, он просто использует этот блок server для обслуживания запроса. Если он найдет несколько блоков, которые отвечают всем требованиям, Nginx выберет один блок на основе директивы server_name.

Важно понимать, что Nginx будет оценивать директиву server_name только тогда, когда ему нужно выбрать один блок из списка блоков, отобранных по директиве listen. Например, если домен example.com размещен на порту 80 по адресу 192.168.1.10, запрос на example.com всегда будет обслуживаться первым блоком в пример ниже, несмотря на директиву server_name во втором блоке.

server {
listen 192.168.1.10;
. . .
}
server {
listen 80;
server_name example.com;
. . .
}

Если же Nginx отобрал несколько блоков с одинаковым уровнем специфичности, далее он проверит директиву server_name.

Директива server_name

Для дальнейшей оценки запросов, имеющих одинаковые определения директивы listen, Nginx проверяет заголовок Host запроса. Это значение содержит домен или IP-адрес, который запрашивает клиент.

Nginx ищет наилучшее совпадение этого значения в директиве server_name каждого блока, прошедшего предыдущий этап отбора. Nginx оценивает эту директиву по этой формуле:

  • Сначала Nginx пытается найти блок server, значение server_name которого точно соответствует значению в заголовке Host запроса. Если такой блок найдется, он будет использоваться для обслуживания запроса. Если Nginx найдет несколько точных совпадений, используется первый найденный блок.
  • Если Nginx не нашел точное совпадение, он попытается найти блок, директива server_name которого начинается со специального символа *. Если Nginx найден такой блок, этот блок будет использоваться для обслуживания запроса. Если Nginx  найдет несколько совпадений, для обслуживания запроса будет использоваться наиболее точное совпадение.
  • Если Nginx не нашел совпадений по специальному символу в начале server_name, он будет искать блок, чье значение server_name заканчивается специальным символом *. Если такой блок найден, он используется для обслуживания запроса. Если Nginx найдет несколько совпадений, для обслуживания запроса будет использоваться наиболее точное из них.
  • Если Nginx не нашел совпадений по специальному символу в конце server_name, он оценивает блоки, чье значение server_name использует регулярные выражения (они определяются символом ~ перед именем). Для обслуживания запроса будет использоваться первый блок, который содержит в server_ name регулярное выражение, которое соответствует заголовку Host.
  • Если найти блок по регулярным выражениям не удалось, Nginx выбирает блок server по умолчанию для этого IP-адреса и порта.

Для каждой комбинации IP-адреса и порта существует блок server по умолчанию, который используется в случае, если веб-сервер не смог найти другой блок. Как правило, это либо первый блок в конфигурации, либо блок, который содержит параметр default_server как часть директивы listen (она переопределяет алгоритм поиска первого совпадения). Для каждой комбинации IP-адреса и порта может быть только одно объявление default_server.

Примеры

Если в конфигурации есть блок с директивой server_name, значение которой полностью совпадает с заголовком Host запроса, запрос передается на обработку этому блоку.

К примеру, если заголовок Host запроса – host1.example.com, веб-сервер выберет для его обслуживания второй блок server:

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

Если Nginx не находит точных совпадений, он будет искать блок, в котором server_name начинается со специального символа. Если Nginx найдет несколько совпадений, для обслуживания запроса будет использоваться наиболее точное из них. Например, если в запросе указан заголовок Host www.example.org, Nginx выберет второй блок:

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

Если найти блок по специальному символу в начале директивы не удалось, Nginx будет искать блок, значение server_name которого заканчивается специальным символом. Если он найдет несколько совпадений, он использует наиболее точное из них. К примеру, для обработки запроса с заголовком Host www.example.com веб-сервер использует третий блок server:

server {
listen 80;
server_name host1.example.com;
. . .
}
server {
listen 80;
server_name example.com;
. . .
}
server {
listen 80;
server_name www.example.*;
. . .
}

Если найти блок по специальному символу не получилось, Nginx будет искать директивы server_name, которые содержат регулярные выражения. Для обработки запроса будет использоваться первый блок, чье регулярное выражение в директиве совпало с заголовком запроса.

К примеру, для обслуживания запроса с заголовком Host www.example.com веб-сервер выберет второй блок server:

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

Если ни один из механизмов поиска не дал результатов, веб-сервер применит блок server по умолчанию.

2: Поиск блока location

Похожий алгоритм Nginx использует для поиска блока location.

Синтаксис блока location

Для начала следует ознакомиться с синтаксисом блока location. Блоки location находятся внутри блоков server (или других блоков location) и используются для определения способа обработки URI запроса (той части запроса, которая идет после имени домена или IP-адреса/порта).

Как правило, блок location имеет такой вид:

location optional_modifier location_match {
. . .
}

location_match в приведенном выше примере указывает, что Nginx должен проверить URI запроса. Наличие или отсутствие модификатора в приведенном выше примере влияет на то, как Nginx будет искать блок location.

Существуют такие модификаторы блоков location:

  • (нет): если в блоке нет модификатора, блок location интерпретируется как префикс. Это означает, что для определения соответствия указанный блок будет сравниваться с началом URI запроса.
  • =: этот блок будет выбран, если URI запроса точно соответствует указанному location-у.
  • ~: такой блок будет интерпретироваться как совпадение по регулярному выражению с учетом регистра.
  • ~*: такой блок будет интерпретироваться как совпадение по регулярному выражению без учета регистра.
  • ^~: если этот блок выбран как наиболее точное совпадение без регулярного выражения, то веб-сервер не будет проводить поиск по регулярному выражению.

Примеры синтаксиса блока location

В качестве примера поиска по префиксу можно использовать следующий блок location для ответа на запросы URI (/site, /site/page1/index.html, или /site/index.html).

location /site {
. . .
}

Ниже вы найдете пример точного совпадения URI. Такой блок всегда будет использоваться для обслуживания URI

/page1. Он не будет отвечать на URI запроса /page1/index.html. Имейте в виду, что если выбран этот блок и запрос обслуживается индексной страницей, произойдет внутреннее перенаправление в другой блок location, который будет фактическим обработчиком запроса.

location = /page1 {
. . .
}

Интерпретация блока location как регулярного выражения с учетом регистра происходит в следующем примере. Этот блок будет использован для обработки запросов для /tortoise.jpg, но не для /FLOWER.PNG:

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

В следующем примере происходит интерпретация блока location как регулярного выражения без учета регистра. Такой блок сможет обработать запросы и для /tortoise.jpg, и для /FLOWER.PNG.

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

Следующий блок отключит поиск по регулярному выражению, если он выбран как наилучшее совпадение без регулярных выражений. Он может обрабатывать запросы для /costumes/ninja.html:

location ^~ /costumes {
. . .
}

Как видите, модификаторы указывают, как следует интерпретировать блок location. Тем не менее, это не определяет алгоритм, который Nginx использует, чтобы решить, какому блоку location отправить запрос.

Выбор блока location

Nginx выбирает блок location аналогично тому, как он выбирает блок server. Он запускает процесс, который определяет лучший блок location для конкретного запроса. Понимание этого процесса является важнейшим требованием для надежной и точной настройки Nginx.

Имея в виду типы объявлений, которые мы рассмотрели выше, Nginx оценивает возможные контексты location путем сравнения URI запроса с каждым из местоположений. Он делает это, используя следующий алгоритм:

  • Сначала Nginx проверяет все блоки location, заданные префиксными строками. Для этого location сравнивается с полной строкой URI.
  • Затем Nginx ищет точное совпадение. Если он находит location с модификатором =, он прекращает поиск и использует найденную конфигурацию.
  • Если точного совпадения не обнаружено, веб-сервер выполняет поиск по неточным совпадениям. Он ищет location с совпадающим префиксом максимальной длины для заданного URI, который затем оценивается таким образом:
    • Если location с совпадающим префиксом максимальной длины содержит модификатор ^~, то Nginx немедленно прекратит поиск и выберет этот блок location для обслуживания запросов.
    • Если location с совпадающим префиксом максимальной длины не содержит модификатора ^~, то Nginx запомнит этот префикс и продолжит поиск.
  • После того, как Nginx нашел и запомнил location с совпадающим префиксом максимальной длины, он приступает к оценке регулярных выражений (с учетом и без учета регистра). Если в location с совпадающим префиксом максимальной длины есть какие-либо блоки location с регулярными выражениями, Nginx поместит их в начало списка регулярных выражений для проверки. Затем Nginx последовательно сравнит блоки с регулярными выражениями. Для обработки будет выбрано первое выражение, которое соответствует URI запроса.
  • Если совпадение с регулярным выражением не найдено, Nginx использует конфигурацию запомненного ранее префиксного location-а.

Важно понимать, что по умолчанию Nginx будет обслуживать регулярные выражения, отдавая предпочтение совпадениям по префиксам. Однако сначала он оценивает префиксные location-ы, позволяя администратору переопределять это поведение, указав location-ы с помощью модификаторов = и ^~.

Также важно отметить, что, хотя префиксные location-ы обычно выбираются на основе префикса максимальной длины (наиболее точного совпадения), Nginx  перестанет оценивать регулярные выражения при обнаружении первого подходящего location-а. Это означает, что расположение в конфигурации блоков location с регулярными выражениями имеет огромное значение.

Оценка блоков location

Теперь нужно разобраться, в каких случаях оценка блоков location переходит к другим location-ам.

В целом, с того момента, как блок location выбран для обслуживания запроса, запрос обрабатывается целиком в этом контексте. Только выбранный блок location и унаследованные директивы определяют, как обрабатывается запрос, а соседние блоки location не могут вмешиваться в этот процесс.

Это общее правило, которое позволит вам спроектировать блоки location предсказуемым образом. Но при этом важно понимать, что бывают случаи, когда определенные директивы запускают новый поиск location-а внутри выбранного блока location. Исключения из правила могут привести к появлению непредсказуемых результатов.

Вот некоторые из директив, которые могут стать причиной такого поведения:

  • index
  • try_files
  • rewrite
  • error_page

Директива index всегда приводит ко внутреннему перенаправлению, если она используется для обработки запроса. Точные совпадения location-а часто используются для ускорения процесса выбора, потому что это немедленно прекратит выполнение алгоритма. Однако если точное соответствие location-а будет каталогом, есть вероятность, что для фактической обработки запрос будет перенаправлен в другое место.

В этом примере первый location совпадает с URI запроса /exact, но директива index, унаследованная блоком, для обработки запроса инициирует внутреннее перенаправление ко второму блоку:

index index.html;
location = /exact {
. . .
}
location / {
. . .
}

Если вы хотите, чтобы в приведенном выше случае запрос обрабатывался первым блоком, вам придется придумать другой метод попадания запроса в каталог. Например, вы можете установить для этого блока неправильный index и включить autoindex:

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

Это один из способов предотвратить перенаправление запроса из первого контекста, но он, вероятно, не подходит для большинства конфигураций. В основном точное совпадение в каталогах может быть полезно для таких операций, как переписывание запроса (что также приводит к новому поиску блока location).

Еще один случай, в котором может начаться новый поиск location-а – это использование директивы try_files. Эта директива говорит Nginx проверить наличие именованного набора файлов или каталогов. Последним параметром может быть URI, на который Nginx сделает внутреннее перенаправление.

Рассмотрим такую конфигурацию:

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

Если в приведенном выше примере запрос сделан для /blahblah, сначала получит запрос первый location. Он попытается найти файл с именем blahblah в каталоге /var/www/main. Если он не сможет найти его, он будет искать файл с именем blahblah.html. Затем он попытается узнать, есть ли в каталоге /var/www/main каталог blahblah/. Если все эти попытки не принесут результатов, запрос будет перенаправлен на /fallback/index.html. Это вызовет новый поиск location-а, и запрос перейдет второму блоку. Он обслужит файл /var/www/another/fallback/index.html.

Также на поиск блоков влияет директива rewrite. Обрабатывая rewrite без параметров или с параметром last, Nginx будет искать новый блок location на основе результатов перезаписи.

Например, если изменить последний пример и добавить в него перезапись, вы увидите, что запрос иногда передается непосредственно второму блоку location, не полагаясь на директиву 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;
}

В приведенном выше примере запрос /rewriteme/hello будет сначала обрабатываться первым блоком location. Он будет переписан в /hello, и веб-сервер будет искать location. В этом случае он снова будет соответствовать первому location-у и обрабатываться директивой try_files (возможно, используя внутреннее перенаправление, чтобы вернуться к /fallback/index.html, если ничего не было найдено).

Однако если запрос сделан для /rewriteme/fallback/hello, первый блок снова будет отвечать запросу. При этом снова применяется перезапись, на этот раз в результате получится /fallback/hello. Затем запрос будет обслуживаться вторым блоком.

Похожая ситуация возникает с директивой return при отправке кодов состояния 301 или 302. Разница в этом случае заключается в том, что она приводит к совершенно новому запросу извне переадресации. Такая же ситуация может возникнуть с директивой rewrite при использовании флагов redirect или permanent.

Директива error_page может привести к внутреннему перенаправлению аналогично тому, как это делает try_files. Эта директива используется для определения действий, которые выполняются при обнаружении определенных кодов состояния. Эти действия, вероятно, никогда не будут выполнены, если установлена директива try_files, так как эта директива обрабатывает весь жизненный цикл запроса.

Рассмотрим такой пример:

root /var/www/main;
location / {
error_page 404 /another/whoops.html;
}
location /another {
root /var/www;
}

Каждый запрос (кроме тех, которые начинаются с /another) будет обрабатываться первым блоком, который будет обслуживать файлы из каталога /var/www/main. Однако если файл не найден (статус 404), произойдет внутреннее перенаправление на /another/whoops.html, что приведет к новому поиску блока location, который в конечном итоге окончится вторым блоком. Этот блок будет обслуживать файл /var/www/another/whoops.html.

Как видите, понимание условий, при которых Nginx запускает новый поиск блока location, может помочь предсказать поведение веб-сервера при выполнении запросов.

Tags: