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?

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

Главное

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

Комментарии

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

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

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