Вопрос или проблема
В моем конфигурационном файле nginx 1.12.2
указано:
upstream app {
server unix:/tmp/app.sock fail_timeout=0;
}
server {
listen 443 deferred;
root /some/dir;
try_files $uri @app;
# Если запрос предназначен для голого домена, просто отдайте индексный пакет
# без прохождения через Rails
#
location = / {
try_files /index.html =404;
}
# Если запрос к префиксу /api, идите напрямую к Rails.
# Этот блок location не строго необходим, но может быть полезным
# местом для добавления заголовков и настроек.
#
location /api/ {
# Дополнительная конфигурация!
try_files @app;
}
# Директория location позволяет создавать очень творческие конфигурации.
# http://nginx.org/en/docs/http/ngx_http_core_module.html#location
#
# Это просто именованное место для использования в try_files.
location @app {
proxy_pass_request_headers on;
proxy_set_header ...
proxy_pass http://app;
}
}
Это не совсем правильно, потому что у него один аргумент:
location /api/ {
# Дополнительная конфигурация!
try_files @app;
}
…но это хорошо передает, что я пытаюсь достичь.
Я предполагаю, что могу заставить try_files
работать, добавив несуществующий файл перед последним аргументом.
Является ли try_files
единственным способом сделать это или есть другой, более идиоматический директив?
Ваша схема не сработает. Когда nginx
определится с окончательным блоком location
для обработки запроса, он будет использовать “настройки и заголовки”, которые находятся в области видимости, которые могут наследоваться от окружающего блока, но не будут включать в себя какие-либо “дополнительные заголовки и настройки” от соседних блоков – независимо от процесса, выбранного для нахождения окончательного блока location
. См. документы request_processing для более подробной информации.
Если у вас есть общие утверждения, применимые к нескольким местоположениям, вы можете переместить их в отдельный файл и включить их там, где это необходимо. Например:
location / {
try_files $uri @app;
}
location /api/ {
# Дополнительная конфигурация!
include my/proxy/conf;
}
location @app {
include my/proxy/conf;
}
См. документ include для более подробной информации.
У нас довольно сложная конфигурация Nginx, и мы хотели использовать именованные места, чтобы сделать все немного более DRY. Из-за этого требования нам пришлось сделать то же самое, что и вам.
Печальная часть в том, что мы не смогли найти прямой способ сделать это, но хорошая новость в том, что вы можете обмануть Nginx с директивой try_files
без потерь в производительности, указывая на первый аргумент /dev/null
:
try_files /dev/null @the_named_location;
Это, безусловно, обходной путь, и обычно я бы сжег любой pull request, содержащий этот хак, и тщательно объяснил разработчику ужасы несоблюдения стандартов и т.д… но после того, как вы убедитесь и примете эту грустную реальность, это действительно делает вашу конфигурацию значительно красивее. Действительно красивее!
Мы решили, что этот “хак” “приемлем”, так как он привел к значительному улучшению с точки зрения качества кода в целом.
(но это все равно иногда режет мне глаза…)
Несколько директив сторонних модулей позволяют напрямую переходить к именованному месту, например, ngx.exec
из lua-nginx-module или echo_exec
из echo-nginx-module (оба модуля включены в пакет OpenResty).
Обновление @ 2025.01.03
Как альтернативу, можно использовать следующую конфигурацию модуля njs:
Фрагмент nginx.conf
:
js_import /path/to/redirect.js;
location /api/ {
# Дополнительная конфигурация!
js_content redirect.redirect;
}
/path/to/redirect.js
:
function redirect(r) {
r.internalRedirect('@app');
}
export default {redirect}
Для некоторых установка модуля njs может быть довольно простой, так как он включен в официальные репозитории nginx.
Однако, используя “ванильный” nginx, единственный способ сделать это – использовать трюк с директивой try_files
. К сожалению, предположение @VictorSchröder о использовании /dev/null
в качестве аргумента try_files
не устранит потери в производительности из-за системного вызова stat
, оно ошибочно, в этом случае это файл /webroot/dev/null
будет “статистическим”. Каждый может проверить это и убедиться в этом, используя следующую инструкцию. Так что нет разницы с общепринятым решением, таким как
try_files /nonexistent @app;
Однако возможно передать пустую строку в качестве параметра try_files
:
try_files "" @app;
Это не устранит системный вызов stat
. Однако в этом случае будет проверена и статистически оценена веб-дерево директория, что, скорее всего, не является существующим физическим файлом на вашем файловой системе. По моему предположению, современные ядра должны кешировать результаты системных вызовов stat
для существующих объектов файловой системы более эффективно, чем для несуществующих. Это только предположение, поэтому если кто-то, читающий это, знает лучше, как ведет себя ядро в этой ситуации, я буду рад услышать любые разъяснения.
Я думаю, последняя часть нуждается в некотором разъяснении. Почему директива try_files
проверяет корневой каталог как физический файл, а не как директорию? Это потому, что аргумент ""
не заканчивается косой чертой. Аргумент, заканчивающийся косой чертой, заставляет try_files
проверять его как директорию, а аргумент, не заканчивающийся косой чертой, заставляет try_files
проверять его как файл. То есть первая проверка в try_files "/" ...
всегда будет успешной (а явная или неявная директива index
выполнит поиск индекса файла на следующем этапе обработки запроса, если место расположения имеет обработчик статического содержимого) если корень местоположения указывает на существующую директорию, и первая проверка в try_files "" ...
всегда будет неуспешной в этом случае.
Также возможно, что веб-дерево сервера будет несуществующей директорией, например, когда nginx используется только для реверс-прокси. Как и многие другие директивы nginx, если не наследуется от предыдущих уровней конфигурации, имеет значение по умолчанию /html
, относительно префикса nginx. Этот префикс указывается во время сборки и может быть проверен с командой nginx -V
(см. аргумент --prefix=...
в настройках компилятора). Если, например, этот префикс равен /etc/nginx
(обычный случай), веб-дерево сервера по умолчанию будет /etc/nginx/html
, что маловероятно, чтобы быть существующей директорией для большинства сред.
Тем не менее, я предпочел бы try_files "" ...
вместо try_files /nonexistent ...
. Если каким-либо образом файл nonexistent
появится в директории webroot (предположим, что это какая-то атака хакера), первое продолжит работать, тогда как второе – нет.
Можно использовать try_files "" ...
в случаях, когда веб-дерево указывается с помощью директивы alias
, особенно для точного или регулярного сопоставления местоположений. Однако я не вижу возможных случаев его использования внутри местоположения с использованием root
, поэтому думаю, что команда разработки nginx может рассмотреть возможность обновления исходного кода модуля try_files
, чтобы полностью устранить этот дополнительный системный вызов stat
в случае такого использования. Это позволит выполнять “безусловные” переходы местоположения без каких-либо дополнительных потерь в производительности, при сохранении совместимости с существующей конфигурацией.
Ответ или решение
Вопрос, который вы подняли относительно перенаправления запроса с одного location на другой в конфигурации Nginx, отражает интересный нюанс, связанный с его возможностями и ограничениями. Используя версию Nginx 1.12.2, вы сталкиваетесь с проблемой, когда необходимо направить запрос из одной location-директивы в другую именованную location. Попробуем подробно разобраться в вашем вопросе и найти оптимальное решение в контексте возможностей Nginx.
Детали конфигурации
Ваш текущий конфигурационный файл сервера Nginx выглядит следующим образом:
upstream app {
server unix:/tmp/app.sock fail_timeout=0;
}
server {
listen 443 deferred;
root /some/dir;
try_files $uri @app;
location = / {
try_files /index.html =404;
}
location /api/ {
try_files @app;
}
location @app {
proxy_pass_request_headers on;
proxy_set_header ...
proxy_pass http://app;
}
}
Проблема с try_files
Вы правильно заметили, что использование try_files
с единственным аргументом @app
не является корректным. Директива try_files
ожидает как минимум два аргумента: первый — список файлов, которые необходимо проверить, второй — файл или location, к которому необходимо направить запрос в случае, если ни один из файлов не был найден. Чтобы обойти это ограничение, было предложено использовать недействительный путь в первом аргументе, например, /dev/null
, что позволит Nginx сразу перейти к указанной именованной location:
try_files /dev/null @app;
Этот подход, несмотря на его хакерский характер, позволяет вам достичь желаемого результата без значительных потерь в производительности.
Альтернативные подходы
Хотя использование try_files
является одним из способов решения вашей задачи, можно рассмотреть и другие варианты:
-
Использование OpenResty или модулей из третьих источников: OpenResty обеспечивает расширенные возможности для Nginx, позволяя использовать модули вроде
lua-nginx-module
, который поддерживает функциюngx.exec
для перенаправления запросов:ngx.exec("@app")
Применение таких модулей расширяет ваши возможности, но требует установки дополнительных компонентов.
-
Использование модуля njs: Он позволяет писать логические блоки в JavaScript для обработки запросов. Например:
js_import /path/to/redirect.js; location /api/ { js_content redirect.redirect; }
function redirect(r) { r.internalRedirect('@app'); } export default {redirect}
Этот подход удобен, если njs уже внедрен в ваше окружение.
Заключение
Для достижения чистоты и сдержанности в конфигурации Nginx часто приходится идти на компромиссы, такие как использование нестандартных приемов try_files
. Вне зависимости от выбора подхода, важно учитывать будущую поддержку и гибкость, особенно если в ваших проектах используется сложная логика маршрутизации. Поскольку технология и инструменты, такие как Nginx, постоянно развиваются, рекомендуется следить за обновлениями и обсуждать найденные решения с коллегами для нахождения наиболее производительных и поддерживаемых решений.
Благодарю за ваш вопрос, и надеюсь, что предложенные варианты помогут выстроить эффективную и удобную конфигурацию для вашего проекта.