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

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

map: таблица вместо условия

Коротко

map - таблица «значение на входе, значение на выходе», которая считается один раз при первом обращении и кешируется до конца запроса. Точные значения всегда сильнее регулярок, где бы те ни стояли. Регистронезависимость ~* работает только на латинице. После перезаписи URI кешированная карта отдаёт старое значение - если это мешает, ставят volatile.

Пишешь карты каждый день - листай до «Теперь сам». Разборы про порядок поиска и про значение, застрявшее до перезаписи.

Регулярка написана выше точного значения. Что вернётся, если на вход придёт ровно abc?

map $http_x_ord $ord {
    "~^abc$"   "регулярка";
    "abc"      "точное";
    default    "-";
}

Порядок строк подсказывает «регулярка». На стенде возвращается точное.

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

Карта - это не цепочка if, читаемая сверху вниз, а словарь с двумя отделениями. Сначала nginx смотрит в отделение точных ключей - там поиск по хеш-таблице, порядок записи роли не играет. Только если ключ не нашёлся, он идёт во второе отделение и там уже перебирает регулярки сверху вниз. Поэтому «переставить строки» ничего не меняет между отделениями и меняет всё внутри второго.

Механизм: порядок поиска

значение исходной переменной точные ключи маски имён при hostnames регулярки, сверху вниз default порядок не важен порядок важен
Отделения перебираются сверху вниз, а строки - только внутри последнего

Правило

Точное значение бьёт любую регулярку. Между регулярками выигрывает написанная выше. Не нашлось ничего - берётся default, а без него пустая строка. Значение карты вычисляется один раз за запрос и запоминается; volatile отменяет запоминание.

Разбор: регистр и кириллица

Две одинаковые по замыслу карты, замер на шести значениях:

map $http_x_key $lat { default "нет"; "~*^BIG$"      "поймала"; }
map $http_x_key $cyr { default "нет"; "~*^БОЛЬШИЕ$"  "поймала"; }
на входе латинская карта кириллическая
BIG поймала нет
big поймала нет
Big поймала нет
БОЛЬШИЕ нет поймала
большие нет нет

Латиница складывается по регистру во всех трёх написаниях. Кириллица - только при точном совпадении байтов: большие не поймалось. Причина в том, что регулярки компилируются без режима юникода, и складывание регистра работает для однобайтовых букв. Правило шире карт: то же самое верно для location ~* и для условий if ($var ~* ...). Русский текст в регулярках сравнивают либо точно, либо перечисляя оба написания.

Разбор: значение, застрявшее до перезаписи

map $uri $кеш     {           default "прочее"; "/шаг1" "шаг1"; "/шаг2" "шаг2"; }
map $uri $volatil { volatile; default "прочее"; "/шаг1" "шаг1"; "/шаг2" "шаг2"; }

location /шаг1 {
    set $снимок "$кеш/$volatil";   # здесь обе переменные вычисляются
    rewrite ^ /шаг2 last;
}
location /шаг2 { return 200 "кеш=$кеш volatile=$volatil\n"; }

Ответ на запрос /шаг1:

↳  выводcurl -s http://localhost:8099/шаг1
кеш=шаг1 volatile=шаг2

Обычная карта вернула значение, посчитанное до перезаписи, - хотя URI давно другой. Кеш существует ради скорости: карта на тысячу строк не должна считаться заново на каждое упоминание переменной. Но если исходная переменная за время обработки меняется - $uri после rewrite, $remote_addr после realip, - нужен volatile.

Тонкость, из-за которой это редко замечают: строка set в примере поставлена специально. Без неё до перезаписи переменную никто не читал бы, кешировать было бы нечего, и обе карты вернули бы одинаково.

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

  • .имя и *.имя - разные маски. При hostnames запись .wild.org ловит и wild.org, и a.wild.org; *.star.org ловит только поддомен, а сам star.org уходит в default. Замер подтверждает оба случая.
  • Переменная в proxy_pass отменяет подстановку хвоста. Полный замер на запросе /x/cart?q=1:
запись что увидел бэкенд
proxy_pass http://$куда; /x/cart?q=1
proxy_pass http://$куда/; /
proxy_pass http://$куда/префикс/; /префикс/
proxy_pass http://$куда$uri; /x/cart - параметры потеряны
proxy_pass http://$куда$request_uri; /x/cart?q=1

Без переменной хвост дописывается сам, с переменной - нет. Отсюда рецепт: адрес с переменной пишут либо совсем без пути, либо с $request_uri в конце. - Карта не знает, откуда взялось значение. map $http_x_forwarded_for ... разбирает то, что прислал клиент, и подделывается тривиально. Адрес берут через realip, а не через карту.

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

Карта - штатный способ развести поведение по признаку, не заводя условий. Классика - апгрейд соединения для WebSocket:

/etc/nginx/conf.d/shop.local.confhttp
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Ровно то же годится для выбора бэкенда, срока жизни кеша, признака «пускать ли в бету». Родственница карты - geo, та же таблица, но ключ - адрес клиента:

/etc/nginx/conf.d/shop.local.confhttp
geo $зона {
    default        чужой;
    127.0.0.0/8    локальный;
    172.16.0.0/12  докер;
}

Замер со стенда: запрос с 172.25.0.1 дал зона=докер. Дальше эту переменную можно класть в ключ лимита, в условие или в пустой ключ - чтобы свои адреса лимит не считал вовсе.

Теперь сам

Карта должна отдавать да на любой заголовок, начинающийся с Bot или бот в любом регистре. Найди в ней две ошибки.

map $http_user_agent $бот {
    "~*^бот"   "да";
    "Bot"      "да";
    default    "нет";
}

Ответ: первая строка не поймает Бот и БОТ - складывание регистра на кириллице не работает, нужно перечислить написания или сравнивать точно. Вторая строка - точное значение, то есть сработает только на заголовке, равном ровно Bot, а не начинающемся с него; нужна регулярка ~*^Bot.

Главное

Точное значение в карте всегда сильнее регулярки, порядок строк важен только среди регулярок. ~* складывает регистр на латинице и не складывает на кириллице. Значение кешируется на запрос: если исходная переменная меняется по дороге, нужен volatile. Переменная в proxy_pass отменяет подстановку хвоста - его дописывают через $request_uri.

Комментарии

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

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

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