Вопрос или проблема
Имея эту простую конфигурацию nginx:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
}
Почему запросы /tmp/foo.php
возвращают ответ Location 3a
, хотя согласно документации регулярное выражение Location 2
должно перехватить запрос, если префиксная локация Location 3
не имеет модификатора ^~
?
Это происходит потому, что фактический алгоритм выбора локации в nginx отличается от того, что описано в документации. Или, чтобы быть более точным, официальная документация не объясняет процесс выбора локации для вложенных локаций, который значительно более сложный. До сих пор я не встречал ни одной статьи на английском языке, объясняющей, как это на самом деле работает, поэтому вот моя попытка внести ясность.
Начнем с конфигурации, приведенной в оригинальном вопросе:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
}
Можно подумать, что запрос /tmp/foo.php
будет обрабатываться с помощью location ~ \.php$ { ... }
(отмеченной как “Location 2”), так как документация прямо говорит:
Чтобы найти локацию, соответствующую данному запросу, nginx сначала проверяет локации, определенные с помощью префиксных строк (префиксные локации). Среди них выбирается локация с самым длинным совпадающим префиксом. Затем проверяются регулярные выражения в порядке их появления в файле конфигурации. Поиск регулярных выражений прекращается при первом совпадении, и используется соответствующая конфигурация.
Давайте проверим это:
> curl http://127.0.0.1/tmp/foo.php
Location 3a
Неожиданно, не так ли?
Документация не упоминает, что после определения самой длинной совпадающей префиксной локации nginx начинает новую итерацию поиска среди своих вложенных локаций, если они есть. Этот процесс продолжается рекурсивно: на каждом шаге применяются одни и те же правила соответствия, и если находится совпадающая вложенная локация с регулярным выражением, а совпадающая вложенная локация с самым длинным префиксом не найдена, nginx прекращает дальнейший поиск и использует эту локацию для обработки запроса.
В нашем примере, для запроса /tmp/foo.php
:
- Nginx сначала определяет самую длинную соответствующую префиксную локацию,
location /tmp { ... }
(отмеченную как “Location 3”). - Начинается новая итерация поиска среди ее вложенных локаций.
- Вложенная
location ~ \.php$ { ... }
(отмеченная как “Location 3a”) соответствует и используется для обработки запроса.
Теперь давайте расширим конфигурацию, добавив четвертую локацию:
location /tmp/foo {
return 200 "Location 4\n";
}
Тестируя тот же запрос:
> curl http://127.0.0.1/tmp/foo.php
Location 2
Что здесь произошло?
- На первом этапе самая длинная совпадающая префиксная локация –
location /tmp/foo { ... }
. - Поскольку у нее нет вложенных локаций, nginx оценивает регулярные локации на том же уровне.
- Регулярная локация
location ~ \.php$ { ... }
совпадает и используется для обработки запроса.
Другие запросы, такие как /tmp/bar.php
, продолжают обрабатываться как и раньше:
> curl http://127.0.0.1/tmp/bar.php
Location 3a
Далее, давайте снова изменим конфигурацию, переместив location /tmp/foo { ... }
внутрь location /tmp { ... }
:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
location /tmp/foo {
return 200 "Location 3b\n";
}
}
Запуская несколько тестов:
> curl http://127.0.0.1/tmp/foo.php
Location 3a
> curl http://127.0.0.1/tmp/foo.html
Location 3b
Пока ничего неожиданного.
Теперь давайте рассмотрим модификатор директивы ^~
. Согласно документации:
Если самая длинная совпадающая префиксная локация имеет модификатор
^~
, то регулярные выражения не проверяются.
Давайте проверим это, используя следующую конфигурацию:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
}
location ^~ /tmp/foo {
return 200 "Location 4\n";
}
Запуская несколько тестов:
> curl http://127.0.0.1/tmp/foo.php
Location 4
> curl http://127.0.0.1/tmp/foo.html
Location 4
> curl http://127.0.0.1/tmp/bar.php
Location 3a
> curl http://127.0.0.1/tmp/bar.html
Location 3
Кажется, что все работает как и ожидалось. Модификатор ^~
на location ^~ /tmp/foo { ... }
гарантирует, что регулярные локации на том же уровне, такие как location ~ \.php$ { ... }
, не могут перехватить запросы, такие как /tmp/foo.php
.
Теперь вот что может действительно вас удивить. Давайте изменим нашу конфигурацию, переместив location ^~ /tmp/foo { ... }
снова внутрь location /tmp { ... }
:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
location ^~ /tmp/foo {
return 200 "Location 3b\n";
}
}
Тестируя тот же запрос:
> curl http://127.0.0.1/tmp/foo.php
Location 2
Упс… Выглядит странно, не так ли? Ну, это, вероятно, последний аспект алгоритма, который необходимо объяснить. После определения самой длинной совпадающей префиксной локации на самом вложенном уровне (в нашем случае, location ^~ /tmp/foo { ... }
, отмеченной как “Location 3b”), если в ней не найдены совпадающие регулярные локации, nginx запоминает эту локацию и начинает подниматься по дереву конфигурации обратно к внешнему уровню. На каждом уровне, который он проходит во время подъема, выполняются следующие действия:
- Если самая длинная совпадающая префиксная локация на текущем уровне не имеет модификатора
^~
, nginx сравнивает запрос со всеми регулярными локациями, определенными на этом уровне. Первая совпадающая регулярная локация, если такая есть, используется для обработки запроса; в противном случае nginx продолжает подниматься. - Если самая длинная совпадающая префиксная локация на текущем уровне имеет модификатор
^~
, nginx пропускает сопоставление запроса с регулярными локациями, определенными на этом уровне, и немедленно поднимается на следующий уровень. Если он уже достиг самого внешнего уровня, используется запомненная самая длинная совпадающая префиксная локация с самого вложенного уровня для обработки запроса.
Вот и все. Имея модификатор ^~
на вложенной префиксной локации (“Location 3b”), это не гарантирует, что запрос не будет перехвачен внешней регулярной локацией (“Location 2”), если внешняя самая длинная совпадающая префиксная локация (“Location 3”) не имеет модификатора ^~
.
П.С. Конечно, следует упомянуть, что если точная соответствующая локация (локация с модификатором =
) найдена на любом уровне во время спуска, nginx немедленно останавливает поиск, и найденная локация используется для обработки запроса. Учитывая это, типичную конфигурацию nginx, такую как
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/path/to/php-fpm/socket;
}
можно значительно оптимизировать, если файл index.php
является единственным обработчиком запросов, что часто встречается во многих случаях:
location = /index.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/path/to/php-fpm/socket;
}
Ответ или решение
Алгоритм выбора местоположения в Nginx
Nginx, один из самых популярных веб-серверов, использует определенный алгоритм для выбора подходящего блока location
при обработке входящих запросов. В то время как официальная документация описывает общие принципы, реальные нюансы, особенно касающиеся вложенных местоположений, могут вызвать путаницу. В этой статье мы подробно рассмотрим алгоритм выбора местоположения в Nginx на примере простой конфигурации.
Пример конфигурации
Рассмотрим следующую конфигурацию Nginx:
location / {
return 200 "Location 1\n";
}
location ~ \.php$ {
return 200 "Location 2\n";
}
location /tmp {
return 200 "Location 3\n";
location ~ \.php$ {
return 200 "Location 3a\n";
}
}
Запрос /tmp/foo.php
Когда мы отправляем запрос на URL /tmp/foo.php
, можно ожидать, что будет использовано местоположение location ~ \.php$ { ... }
, однако получаем ответ Location 3a
. Как это объяснить?
-
Поиск самого длинного префикса: Nginx сначала ищет самое длинное подходящее префиксное местоположение. Для такого запроса это
location /tmp { ... }
, что соответствуетLocation 3
. -
Новая итерация поиска: После нахождения местоположения начинается новая итерация поиска для вложенных местоположений.
-
Поиск по вложенным местоположениям: Из-за нахождения совпадения с
location ~ \.php$ { ... }
в рамкахlocation /tmp
, используетсяLocation 3a
.
Подробности алгоритма
Алгоритм выбора местоположений в Nginx можно разбить на несколько ключевых этапов:
-
Первичный поиск: Nginx ищет местоположение с самым длинным префиксом. Если оно находит, оно "запоминает" его.
-
Итеративный поиск вложенных местоположений:
- Если найденное местоположение имеет вложенные блоки
location
, Nginx осуществляет поиск снова, но уже среди вложенных. - Если найдено вложенное регулярное выражение, и не существует совпадений для префиксов, используется вложенное местоположение.
- Если найденное местоположение имеет вложенные блоки
-
Восхождение к верхним уровням конфигурации: Если на одном из уровней не было совпадения, Nginx поднимается к следующему уровню и производит аналогичные проверки.
Исключения и модификаторы
Использование модификатора ^~
дает дополнительную нагрузку на процесс. Если на уровне самого длинного совпадения присутствует модификатор ^~
, проверки регулярных выражений не выполняются.
Обновленная конфигурация
Предположим, добавляем другой уровень:
location /tmp/foo {
return 200 "Location 4\n";
}
Тогда при отправке запроса /tmp/foo.php
происходит следующее:
-
Поиск по префиксным местоположениям: Первое найденное местоположение – это
/tmp/foo
, и оно совпадает. -
Регулярные выражения: В соответствии с правилами, Nginx ищет соответствие среди регулярных выражений на этом уровне, и
Location 2
переходит в обработку.
Таким образом, результатом станет Location 2
.
Сложные конфигурации и поведение с вложенностью
Если мы поместим location ^~ /tmp/foo { ... }
внутрь блока /tmp
, мы увидим, что на /tmp/foo.php
всё равно будет использовано Location 2
, потому что алгоритм не завершает поиск, если соответствие не найдено, и продолжает восходить вверх.
Заключение
Алгоритм выбора местоположений в Nginx сложнее, чем часто принято считать. Непонимание его работы может привести к неправильной конфигурации и ожиданиям, что спрос на диагностику ошибок может возрасти, особенно в сложных веб-приложениях. Важно разбираться в этих нюансах, чтобы гарантировать, что ваши настройки Nginx ведут к ожидаемым результатам.
Применение местоположений в Nginx требует глубокого понимания его поведений, что позволяет администраторам серверов обеспечивать оптимальную работу приложений и сервисов, использующих этот мощный инструмент.