1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

Именованные location

Коротко

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

  • Ссылаются двумя директивами: try_files последним аргументом и error_page через =.
  • Классика: try_files $uri $uri/ @backend - статика с диска, всё остальное в приложение.
  • Смысл имени - написать блок проксирования один раз и ссылаться из нескольких мест.
  • error_page не перехватывает ответ бэкенда без proxy_intercept_errors on. Он ловит только то, что породил сам nginx, - и это ломает самый популярный рецепт страницы обслуживания.
  • Вложить внутрь @ ничего нельзя: это конечная точка маршрута.

Если про proxy_intercept_errors уже знаешь - листай до «Теперь сам».

Сначала ответь сам

Хотим показывать свою страницу вместо ошибки бэкенда:

/etc/nginx/conf.d/shop.confhttp server
location /api/ {
    proxy_pass http://127.0.0.1:8080;
    error_page 502 504 = @maintenance;
}

location @maintenance { return 503 "идут работы"; }

Приложение живо, но на этот запрос само отвечает 502 - например, у него отвалилась база. Что увидит клиент?

Инстинктивный ответ - «идут работы», ради этого конфиг и написан. Настоящий - 502 и страницу от приложения. Именованный блок не сработает.

На что это похоже

Именованный блок - это внутренний номер, а не дверь с улицы.

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

Отсюда и смысл: в такой «кабинет» кладут то, что нужно нескольким отделам сразу, - чтобы не держать три одинаковых инструкции, которые однажды разойдутся.

Механизм: две двери внутрь

/etc/nginx/conf.d/shop.confhttp server
location / {
    try_files $uri $uri/ @backend;
}

location @backend {
    proxy_pass http://127.0.0.1:8080;
}
location / location /static/ try_files ... @backend error_page 404 = @backend @backend proxy_pass запрос с улицы сюда - нельзя Никакой URI не попадёт в @backend сам, даже запрос на /@backend.
Именованный блок не участвует в выборе. Внутрь ведут ровно две директивы, и обе вызывают его изнутри.

Читается конфиг так: «есть такой файл - отдай его; есть такой каталог - отдай индекс из него (файл, который отдают вместо каталога, по умолчанию index.html); ничего нет - иди в @backend». Вторая дверь, error_page, устроена так же: location /static/ { error_page 404 = @backend; } отдаёт файл с диска, а за несуществующим идёт в приложение - проверено. Это конфиг, который нужен большинству приложений.

Директиву в try_files не вставить - последним аргументом он принимает либо URI, либо код вида =404 («ответить кодом 404»; знак тот же, что у точного location, смысл свой), либо именованный блок. Имя и существует затем, чтобы блок с настройками лежал в одном месте:

/etc/nginx/conf.d/shop.confhttp server
location /       { try_files $uri @backend; }
location /admin/ { try_files $uri @backend; }

location @backend {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

Две строки proxy_set_header сообщают приложению имя сайта и адрес клиента (зачем это нужноРазбирается в разделе 7, глава «Заголовки: Host, X-Real-IP, X-Forwarded-*»: Что бэкенд перестаёт видеть). Настройки проксирования написаны один раз. Без имени их пришлось бы продублировать - и однажды поправить только в одном месте.

Правило

В именованный блок нельзя попасть по URI: только из try_files или error_page. Держат в нём то, что нужно из нескольких мест.

Разбор: как это работает живьём

Проверено на nginx 1.31.5. Первый server-блок - подставной бэкенд, как в разделе про язык конфига; на диске, в /srv/site, лежит только real.txt:

/etc/nginx/conf.d/shop.confhttp
server {
    listen 8080;
    return 200 "БЭКЕНД $request_uri\n";
}
server {
    listen 80;
    root /srv/site;
    location / { try_files $uri $uri/ @backend; }
    location @backend { proxy_pass http://127.0.0.1:8080; }
}
$  команда
for u in /real.txt /net-takogo /@backend; do printf '%-14s -> ' $u; curl -s localhost$u; done
↳  выводfor u in /real.txt /net-takogo /@backend; do printf '%-14s -> ' $u; curl -s localhost$u; done
/real.txt      -> файл с диска
/net-takogo    -> БЭКЕНД /net-takogo
/@backend      -> БЭКЕНД /@backend

Первый запрос закрыт файлом, второй ушёл в приложение. Третий - проверка на то, что имя действительно не адрес: /@backend обработался как обычный путь и ушёл в приложение вместе со всеми остальными.

Разбор: где наивное правило подводит

Наивное правило: «error_page 502 = @maintenance показывает мою страницу, когда бэкенд отвечает 502».

Разница в том, кто породил ошибку. Три случая, все проверены. Второй подставной бэкенд на порту 8082 на всё отвечает ошибкой - server { listen 8082; return 502 "502 ОТ БЭКЕНДА"; }, а на порту 9999 не слушает никто:

/etc/nginx/conf.d/shop.confhttp server
location /otvechaet/ {                       # бэкенд сам отвечает 502
    proxy_pass http://127.0.0.1:8082;
    error_page 502 504 = @maintenance;
}
location /perehvat/ {                        # то же плюс перехват
    proxy_pass http://127.0.0.1:8082;
    proxy_intercept_errors on;
    error_page 502 504 = @maintenance;
}
location /net-bekenda/ {                     # бэкенда нет вовсе
    proxy_pass http://127.0.0.1:9999;
    error_page 502 504 = @maintenance;
}
$  команда
for u in /otvechaet/x /perehvat/x /net-bekenda/x; do echo -n "$u -> "; curl -s -w "  код=%{http_code}\n" localhost$u; done
↳  выводfor u in /otvechaet/x /perehvat/x /net-bekenda/x; do echo -n "$u -> "; curl -s -w " код=%{http_code}\n" localhost$u; done
/otvechaet/x -> 502 ОТ БЭКЕНДА  код=502
/perehvat/x -> идут работы  код=503
/net-bekenda/x -> идут работы  код=503

-w велит curl напечатать после тела код ответа. Знак = в error_page 502 504 = @maintenance значит «код возьми из того, куда переводишь»: @maintenance отвечает 503, его клиент и получает. 502 - бэкенд ответил ошибкой или мусором, 504 - его не дождались, 503 - «сервис временно недоступен», честный код для работ.

Первая строка и есть ловушка. error_page перехватывает ошибки, которые породил сам nginx: не смог соединиться, не дождался ответа, не нашёл файл. Ответ, пришедший от приложения, он по умолчанию пропускает как есть - вместе с его телом и его кодом.

Включает перехват proxy_intercept_errors on. Перехватываются только коды, перечисленные в error_page: замер с тем же error_page 502 504 показал, что осмысленный 404 приложения доезжает до клиента нетронутым. Опасность в том, что список легко расширить - error_page 404 403 500 502 504 = @maintenance, - и тогда твоя страница съест 404 и 403 с телом, которое приложение специально сформировало для клиента. Поэтому перехватывают точечно - в том location, где приложение заведомо не умеет отвечать по-человечески.

Что ломается без этого

  • Страница обслуживания не показывается. Ошибку вернуло приложение, а не nginx. Нужен proxy_intercept_errors on.
  • После включения перехвата пропали осмысленные 404 приложения. Обратная сторона: перехватывается всё, что перечислено в error_page. Сузь список кодов или сам location.
  • location "..." cannot be inside the named location "@...". Внутрь именованного блока вложить нельзя ничего.
  • Запрос неожиданно уходит в приложение. В try_files последним аргументом стоит имя - значит, туда попадает всё, что не нашлось на диске, включая опечатки в адресах.

Зачем это в работе

Именованные блоки берут в двух ситуациях, и обе повторяются на каждом проекте.

Первая - приложение с фронтендом: статика уходит с диска, остальное в код. Одна строка try_files $uri $uri/ @backend заменяет любые попытки перечислить маршруты приложения в конфиге - и не устаревает, когда во фронтенде появляется новый адрес.

Вторая - страница обслуживания, которая показывается вместо ошибки. Тут важно проверять её не рассуждением, а опытом: погасить бэкенд и посмотреть, что приехало. Ровно на этой проверке и вскрывается разница между «бэкенд лежит» и «бэкенд ответил ошибкой» - два случая, которые выглядят одинаково в мониторинге и по-разному в конфиге.

Теперь сам

1. Почему try_files $uri $uri/ @backend; лучше, чем try_files $uri $uri/ /index.html; для приложения с сервером на бэкенде?

Второй вариант отдаёт файл index.html с диска - это годится для фронтенда, который целиком собран в статику. Если за адресами стоит серверный код, ему надо передать сам запрос, а сделать это может только блок с proxy_pass.

2. Что произойдёт с запросом /@maintenance, если в конфиге есть location @maintenance?

Обработается как обычный путь: @maintenance в адресе - просто набор символов. Пойдёт в тот блок, который выиграет обычный выбор, - скорее всего, в location /.

3. Приложение отдаёт 404 со своей красивой страницей, а nginx на несуществующий файл отдаёт голый 404. Как сделать одинаково, не сломав первое?

Написать error_page 404 /404.html; в том location, который отдаёт файлы с диска, и не включать proxy_intercept_errors в блоке проксирования. Тогда свою страницу показывает nginx там, где ошибку породил он, а ответы приложения доезжают до клиента нетронутыми.

Главное

location @имя - подпрограмма: сам по себе не выбирается никогда, попасть внутрь можно только из try_files или error_page. Держат в нём то, что нужно из нескольких мест. И помни: error_page ловит ошибки самого nginx, а ответ бэкенда перехватывается только с proxy_intercept_errors on.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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