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

# теория · шаг 1 из 4

Четыре формы имени и их приоритет

Коротко

Среди блоков, прошедших отбор по адресу и порту, сайт выбирается по заголовку Host. Путь на выбор не влияет.

  • Четыре формы имени с жёстким порядком: точное → маска *.имя → маска имя.* → регулярка.
  • Регулярка проигрывает ЛЮБОЙ маске, хотя выглядит самой точной.
  • В каждой группе масок побеждает самая длинная; *.имя проверяются до имя.*.
  • *.shop.local не совпадает с самим shop.local - отсюда «с www работает, без www нет».
  • Скобочные группы регулярки становятся переменными: на этом делают мультитенантность (один блок на много клиентов) в три строки.

Если порядок «точное, маска, маска, регулярка» помнишь наизусть - листай до «Теперь сам».

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

На сервере два блока:

/etc/nginx/conf.d/shop.confhttp
server { listen 80; server_name ~^api\d+\.shop\.local$; return 200 "API\n"; }
server { listen 80; server_name *.shop.local;           return 200 "сайт\n"; }

Запрос приходит на api7.shop.local. Кто ответит?

Инстинктивный ответ - блок с регуляркой: он написан первым, он точнее, он буквально про api7. Настоящий - «сайт». Регулярка не сработает никогда, и блок API мёртв с момента написания.

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

Представь сортировку писем в большом здании.

Сначала ищут точный адрес - фамилию на конверте. Нашли - письмо ушло, дальше никто не смотрит. Не нашли - смотрят на отдел: «всё, что для бухгалтерии». Если таких правил несколько, берут самое узкое: «для бухгалтерии второго этажа» точнее, чем «для бухгалтерии».

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

Регулярка в nginx - тот самый человек. Не потому что слабее, а потому что дороже.

Механизм: четыре ступени и порядок между ними

http › server
server_name shop.local;              # 1. точное имя
server_name *.shop.local;            # 2. маска в начале
server_name shop.*;                  # 3. маска в конце
server_name ~^shop\d+\.local$;       # 4. регулярное выражение

Это четыре варианта для разных блоков, а не один блок. В регулярке ~ в начале - знак «дальше регулярка», ^ - начало имени, \d+ - одна или больше цифр, \. - буквальная точка, $ - конец имени. Звёздочка в маске заменяет одну или несколько частей имени - кусков между точками.

1. точное имя - нашлось, поиск закончен немедленно 2. маска *.имя - из подошедших берётся самая ДЛИННАЯ 3. маска имя.* - тоже самая длинная 4. регулярка - первая совпавшая ПО ПОРЯДКУ в конфиге Спускаются только вниз и только если на ступени никто не подошёл. Место строки в файле решает лишь внутри четвёртой ступени.
Ступени проходят сверху вниз. Регулярка - последняя, поэтому маска отменяет её всегда, а не иногда.

Внутри второй и третьей ступени решает длина. Для img.cdn.shop.local маска *.cdn.shop.local победит *.shop.local, потому что длиннее, - независимо от того, какая написана выше.

Правило

Точное имя → маска *.имя → маска имя.* → регулярка. Среди масок побеждает самая длинная, среди регулярок - первая по порядку в конфиге. Любая маска сильнее любой регулярки.

Разбор: шесть запросов и живой ответ

Собрал конфиг, где блоки написаны в «неудобном» порядке - регулярка сверху, точное имя снизу, - и прошёл по нему запросами. Проверено на nginx 1.31.5.

/etc/nginx/conf.d/shop.confhttp
server { listen 80; server_name ~^api\d+\.shop\.local$; return 200 "4-regex\n"; }
server { listen 80; server_name shop.*;                 return 200 "3-tail\n"; }
server { listen 80; server_name *.shop.local;           return 200 "2-wild\n"; }
server { listen 80; server_name *.cdn.shop.local;       return 200 "2-wild-long\n"; }
server { listen 80; server_name shop.local;             return 200 "1-exact\n"; }
$  команда
for h in shop.local www.shop.local img.cdn.shop.local api7.shop.local shop.test nomatch.example.com; do printf '%-22s -> ' $h; curl -s -H "Host: $h" localhost/; done
↳  выводfor h in shop.local www.shop.local img.cdn.shop.local api7.shop.local shop.test nomatch.example.com; do printf '%-22s -> ' $h; curl -s -H "Host: $h" localhost/; done
shop.local             -> 1-exact
www.shop.local         -> 2-wild
img.cdn.shop.local     -> 2-wild-long
api7.shop.local        -> 2-wild
shop.test              -> 3-tail
nomatch.example.com    -> 4-regex

Три строки стоит разобрать отдельно.

img.cdn.shop.local подошёл под обе маски, победила длинная - хотя написана она ниже.

api7.shop.local ушёл в маску, а не в регулярку. Это и есть ответ на вопрос из начала урока: блок API недостижим.

nomatch.example.com не подошёл никуда и всё равно получил ответ - от блока с регуляркой. Не потому, что регулярка совпала, а потому что этот блок написан первым на своём порту. Об этом следующая глава, и это самое неприятное в теме.

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

Наивное правило: «*.shop.local - это про весь домен».

↳  выводcurl -s -H "Host: shop.local" localhost/
1-exact

Убери из конфига блок с точным именем - и shop.local перестанет попадать куда надо: замер отправил его в 3-tail, маску shop.*. Звёздочка заменяет одну или несколько частей имени, но не пустоту. *.shop.local совпадает с www.shop.local и a.b.shop.local и не совпадает с shop.local.

Отсюда самый частый симптом в теме: сайт открывается с www и не открывается без него, или наоборот. Лечится перечислением, а не звёздочкой:

/etc/nginx/conf.d/shop.confhttp › server
server_name shop.local www.shop.local;

Имена разделяют пробелом, перечислять можно сколько угодно.

Второе, что стоит знать про регулярки: раз они дороги, брать их надо ради того, чего маска не умеет, - запоминать часть имени.

/etc/nginx/conf.d/tenants.confhttp
server {
    listen 80;
    server_name ~^(?<tenant>[a-z]+)\.tenants\.local$;
    root /var/www/tenants/$tenant;
    add_header X-Tenant $tenant always;
}

(?<tenant>…) - скобочная группа с именем: всё, что совпало с [a-z]+ (одна или больше латинских букв), попадает в переменную $tenant. Заголовок X-Tenant добавлен, чтобы увидеть её значение:

$  команда
curl -sI -H "Host: a.tenants.local" localhost/ | grep X-Tenant
↳  выводcurl -sI -H "Host: a.tenants.local" localhost/ | grep X-Tenant
X-Tenant: a

Один блок обслуживает сколько угодно клиентов, и добавление нового - это создание каталога /var/www/tenants/имя с index.html, а не правка конфига с перезагрузкой. Пока каталога нет, nginx отвечает 404 - замер на b.tenants.local.

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

  • Блок с регуляркой никогда не срабатывает. Проверь, нет ли маски, которая накрывает те же имена. Маска победит.
  • Сайт открывается с www и не открывается без него. В server_name только маска; допиши точное имя.
  • Запрос уходит не в тот блок из двух похожих. Сравни длину масок: решает она, а не порядок строк.
  • nginx -t в порядке, а сайт отдаёт чужие страницы. Host не совпал ни с одним именем, и ответил первый блок на порту.

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

Отладка тут устроена приятно: Host - обычный заголовок, и его можно подставить руками, не трогая DNS (систему, которая превращает имя в адрес) и не дожидаясь, пока новая запись в ней разойдётся по миру.

Так проверяют новый сайт до того, как на него направлен домен, и так же ловят «блок не срабатывает»: подставляешь имя по очереди и смотришь, кто ответил. Чтобы ответивший блок был виден сразу, в него на время добавляют метку:

/etc/nginx/conf.d/shop.confhttp › server
add_header X-Block "shop.local" always;
$  команда
curl -sI -H "Host: api7.shop.local" localhost/ | grep X-Block

На удалённом сервере вместо localhost пишут его адрес.

Это тот же приём, что из раздела про переменные, и на десятке похожих блоков он экономит больше всего времени.

Теперь сам

1. Нужно, чтобы shop.local и все его поддомены вели на один сайт. Как написать server_name?

http › server
server_name shop.local *.shop.local;

Обе формы в одной строке. Маска не покрывает голое имя, поэтому оно пишется отдельно.

2. В конфиге две регулярки, обе подходят под api7.shop.local. Какая сработает?

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

3. Почему server_name не влияет на выбор, если запрос пришёл на порт 8080, а блок слушает 80?

Потому что отбор по адресу и порту идёт раньше. Блок с чужим портом выбывает до того, как имена вообще начинают сравнивать.

Главное

В пределах выбранного адреса и порта точное имя имеет приоритет перед масками, а маски перед регулярными выражениями. Проверяй также имя без поддомена: маска *.shop.local его не покрывает. Среди регулярных имён порядок записи влияет на выбор первого совпадения.

Комментарии

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

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

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