[Из песочницы] Раскрываем возможности map в nginx
map — мощная директива, которая может сделать ваши конфиги простыми и понятными.Возможно, это самая недооцененная директива, из за того, что не все знают всех её возможностей.Она в компактной форме помогает обрабатывать переменные, GET параметры, заголовки, куки и наборы бекендов (upstream).Попробую раскрыть её возможности хабрапользователям.Для простоты, в примерах директива map будет соседствовать с директивами из location.В реальном конфиге место map — в блоке http.Краткое описание map Это директива, которая устанавливает значение одной переменной (правой), в зависимости от другой (левой).Выглядит вот так: map $arg_one $var_two { «one» «two»; } В левой части можно использовать регулярные выражения, включая именованные выделения.В правой части могут быть строки, переменные и regexp выделения из левой части.Директива map описывается в блоке http.Полное описание можно найти в документации. Замена if на map Особенности if в nginx if в nginx реализован по своему, достаточно неочевидно.Вкратце это описано на специальной странице.Насколько я понимаю, if в nginx — это из разряда, когда мир крутится вокруг вас, а не наоборот.На каждый if в location, nginx генерирует у себя два конфига, с if=true и с if=false.Плюс, некоторые директивы ведут себя странно, или вообще не работают рядом с if в одном location.Поэтому при работе с if всегда есть шанс совсем не того поведения, которое вы ожидали.Для гарантированного поведения, if лучше заменять на map. Рассмотрим примеры замен, от простого к сложному.Установить переменной одно значение Для этого у вас, скорее всего, уже написана такая конструкция: if ($arg_one = «one») { set $var_two «two»; } Вы можете это сделать с помощью map: map $arg_one $var_two { «one» «two»; } И в нужном месте просто использовать $var_two.Даже в таком простом случе у map есть плюсы: не важно, сколько ещё if, или других директив окажется в location, эта переменная получит свое значение; для nginx это менее затратно, так как значение переменной будет вычислено только в момент использования; Установить переменной одно из нескольких значений Предположу, что для этого уже прибегают к map, но иногда можно встретить вариант с несколькими if: if ($arg_one = «one») { set $var_two «two»; }
if ($arg_one = «three») { set $var_two «four»; } Вы можете это сделать с помощью map: map $arg_one $var_two { «one» «two»; «three» «four»; } Уже виден выигрыш по компактности и читаемости.Если нужно будет ещё редактировать условия, вам нужно поправить строчку в map.Вложенные if (зависимость от нескольких условий) Я находил в списках рассылки nginx вопросы насчет вложенных if, когда нужно учитывать несколько условий.Вложенные if делать нельзя, можно сделать костыль из нескольких if.А можно написать один map.Возможно, вы не знаете, но в исходной части (где первая переменная) можно указывать не одну переменную, а целый текст, содержащий несколько переменных, в кавычках.Например, вам нужно блокировать пользователей с user-agent «HackYou», долбящихся «POST» запросом по адресу »/admin/some/url».Это, в принцципе, можно сделать с помощью if:
if ($http_user_agent ~ «HackYou») { set $block «A»; }
if ($method = «POST») { set $block »${block}B»; }
if ($uri = »/admin/some/url») { set $block »${block}C»; }
if ($block = «ABC») { return 403; } Вы можете это сделать с помощью map: map »$http_user_agent:$method:$uri» $block { «HackYou: POST:/admin/some/url» »1»; }
if ($block) { return 403; } Двоеточие — просто для удобства восприятия.В этом примере возникает «точка невозврата» в сторону map.Если количество условий растет (например, несколько user-agent), реализовать их набором if уже не получится, или это будет слишком громоздкая конструкция.http заголовки Если вам нужно добавлять заголовки в зависимости от каких-то условий, с if это может стать проблемой.Например, такая конструкция:
if ($arg_a = »1») { add_header X-one «one»; } add_header X-two «two»; Будет отдавать только один заголовок (X-one, если arg_a = true, и X-two, если false).Это недостаток add_header и разработчики не будут это исправлять.А если у вас несколько if с заголовками, может и не получиться добавить несколько разных заголовков одновременно.Но тут на помощь приходит map:
map $arg_a $header_one { »1» «one»; }
add_header X-one $header_one; add_header X-two «two»; Несколько заголовков — несколько map.Если переменная будет пустой, nginx просто не создаст заголовок.Вообще, в случае с if может помочь модуль headers_more, он лишен недостатка add_header насчет if.Модуль headers_more интересен сам по себе, с его помощью можно гибко управлять любыми заголовками, как ответа, так и запроса (на бекенд).В паре с директивой map этот модуль может реализовывать многие хотелки, включая генерацию нескольких кук.Выбор upstream В директивах типа proxy_pass (fastcgi_pass и прочие *_pass), где можно указывать upstream в качестве бекенда, можно использовать переменные.Т.е. работает такое определение: proxy_pass http://$php_backend; Об этом сказано в документации: В этом случае имя сервера ищется среди описанных групп серверов и если не найдено, то определяется с помощью resolver«а.
В паре с map, это дает нам поле для фантазии.Вот грубый пример — допустим, нам нужно такое: Есть кука userid.Если в её значении первое число от 0 до 4, то посылать запросы на upstream old_php_backend.Если с 5 до 9 — на new_php_backend.Если куки нет, или она пустая, или первый символ — не число, то на default_php_backend.Обычно делается с помощью if, rewrite и нескольких location:
location /some/url/ { if ($cookie_userid ~ »~^[0–4]») { rewrite ^(.+)$ /old/$1 last; } if ($cookie_userid ~ »~^[5–9]») { rewrite ^(.+)$ /new/$1 last; }
proxy_pass http://default_php_backend; }
location /old/some/url/ { internal; rewrite ^/old/(.+)$ $1 break; proxy_pass http://old_php_backend; }
location /new/some/url/ { internal; rewrite ^/new/(.+)$ $1 break; proxy_pass http://new_php_backend; } Использование map все упрощает: map $cookie_userid $php_backend { »~^[0–4]» «old_php_backend»; »~^[5–9]» «new_php_backend»; default «default_php_backend»; }
location /some/url/ { proxy_pass http://$php_backend; } Это действительно работает, применяется на нагруженном сервисе, и с этим не наблюдается проблем.Главное — не забывать про default, чтобы всегда было, куда направить запрос.Этот прием можно применять и с geo/split_clients.Например, в split_clients выделить 1% запросов и направить их на отдельный бекенд для тестов.Переменную из map можно использовать в другой map Такой код работает: map $arg_a $var_a { »0» »1»; }
map $var_a $var_b { »1» »2»; }
map $var_b $var_c { »2» »3»; } Когда вы обратитесь к $var_c, последовательно сработает три директивы map.При GET параметре a=0, $var_c будет содержать »3».nginx прожевывает цепочку из 12 map’ов, дальше я не пробовал.Можете попробовать, ради интереса, узнать максимальную длинну цепочки.Обычно мне хватает пары map’ов, которые зависят друг от друга.Мне это бывает полезно для формирования сложных заголовков и кук средствами nginx (когда надо добавить текст в заголовок, на бекенд отправить один заголовок, а клиенту — другой).Это может пригодиться, как развитие примера с вложенными if.В одном map вычисляем $var_a, в другом map вычисляем $var_b, а третий map зависит от »$var_a: var_b».Так же map работает с перменными из geo и split_clients.В geo и split_clients результирующей переменной можно присвоить только простую строку, нельзя использовать переменные или регулярные выражения.Если вам, в зависимости от ip, нужно что-то посложнее простой строки, geo+map помогут вам.А связка split_clients+map поможет вам, например, гибко изменить заголовки для 1% пользователей.
Например, так:
split_clients »${remote_addr}XXX» $test_percent { 1% »1»; * »0»; }
map »$test_percent:$http_user_agent» $test_mobile_users { »~*^1:.*iphone.*» «X-tester: iphone»; }
more_set_input_headers $test_mobile_users; Если ip пользователя попал в тестовый 1% пользователей, и в его user-agent есть слово iphone, то вместе с запросом на бекенд отправляется заголовок «X-tester: iphone».Разработчикам остается отреагировать на этот заголовок и отдавать тестовую версию сайта для айфонов.Заключение Как видите, map помогает делать сложную логику малым количеством команд.Она позволяет избавиться от if в большинстве случаев.А совместно с другими директивами, творит хитрые преобразования в несколько строк.Я надеюсь, эти возможности помогут вам, с одной стороны, сократить конфиги, а с другой — реализовать хитрые хотелки.
