К нам пришла необходимость сделать свой шардинг с анализом содержимого запроса и proxy_pass. Логика нашей системы такова, что мы можем распределить отдельные части БД и обработчиков API по разным серверам и по входящим запросам от клиентов перенаправлять запрос на соответствующий сервер в зависимости от идентификатора проекта. Выглядеть это должно примерно так:
Исходные условия следующие:
Задача: настроить NGINX так, чтобы по содержимому поля uniqid перенаправлять запрос на соответствующий проекту сервер.
Ограничения:
Если у вас нет времени на изучение неработающих способов, прокручивайте сразу до заголовка “Готовое решение” и копируйте.
Итак.
Первое, что напрашивается – директива map в NGINX. Чтобы не тратить время: оно не работает. Если бы у вас были только GET-запросы, тогда задачу можно было бы решить так:
map $arg_uniqid $shard_id {
89T2IU3YT18FTIU1TGEIU1 1;
TFISDFY92837TRHLSFSDFF 2;
0SODHFSKDHFSIDF78TRFKG 0;
...
}
И уже в секции location на основании переменной $shard_id делать соответствующий proxy_pass на нужный бэкенд.
Вот только у нас есть еще POST-запросы (REST API, епта). А значит, для получения значения uniqid нужно парсить содержимое body в запросе. И тут начинаются танцы: нельзя просто так взять и получить значение аргумента из POST-запроса.
Исходя из ограничения № 1 (минимальное вмешательство), ставить Lua или другие ненативные модули на NGINX не хочется. Исходя из ограничения № 3 (время развертывания) при сборке нового сервера необходимо сделать как можно меньше телодвижений.
На помощь приходит модуль Perl для NGINX. Как установить NGINX с Perl, избегая ручной компиляции, читайте в этой статье.
Чтобы не тратить время: perl_set не сработает. А ведь было бы очень круто написать что-то вроде такого:
perl_set $uniqid ‘
sub {
my $r = shift;
if ($r->request_method eq “GET”) {
if($r->args =~ /uniqid=([a-z0-9A-Z]+)/ ) {
my $uniqid = “$1”;
return $uniqid;
}
} else {
if($r->request_body =~ /uniqid=([a-z0-9A-Z]+)/ ) {
my $uniqid = “$1”;
return $uniqid;
}
}
}
’;
Вот только perl_set можно использовать только внутри секции http, а request_body доступен только в секции location. Да и map работает только в секции http. То есть все нужные для этой операции вещи работают в таких местах, что связи между ними нет никакой.
Впору бы отчаяться, но спасатели не сдаются.
Раз нельзя совместить несовместимое, значит несовместимое нужно выкинуть. Убираем map: будем делать сращивание ручками. Убираем perl_set: не будем передавать значение переменной из Perl в конфиг NGINX. Сделаем все по-перловому.
Раз все данные доступны только в location, там и будем работать.
Пишем секцию perl в location. В ней определяем значение uniqid. Так как из этой секции наружу вернуть значение нельзя, то будем делать редиректы с помощью perl internal_redirect. Если определили uniqid и по нему определили шард, редиректим запрос на location, соответствующий этому шарду. Если не нашли uniqid или он оказался левым, просто кидаем 403 ошибку – мы этого человека не знаем и разговаривать нам с ним не о чем.
Одна особенность:
$r->request_body сразу в location недоступен. Его нужно принять. Поэтому в коде присутствует подпрограмма post.
server { listen 80; listen 443 ssl; server_name api.domain.com; keepalive_timeout 5; root /home/rails/api.domain.com/current; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /etc/nginx/ssl/domain.com.chained.crt; ssl_certificate_key /etc/nginx/ssl/domain.key; access_log /var/log/nginx/api.domain.com-access.log; error_log /var/log/nginx/api.domain.com-error.log warn; error_page 500 502 503 504 /500.html; location ~ ^/(assets|images|javascripts|stylesheets|system|favicon\.ico|robots\.txt)/ { return 444; } location / { try_files $uri @app; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Origin $http_origin; } location @app { perl ' sub { my $r = shift; my $request_shop_id = ""; if ($r->request_method eq "GET") { if($r->args =~ /shop_id=([a-z0-9A-Z]+)/ ) { my $shop_id = "$1"; my $shard_id = find_shard($shop_id); if($shard_id ne "") { $r->internal_redirect("/shard" . $shard_id . $r->uri . "?" . $r->args); return; } } $r->internal_redirect("/shard_not_found"); } else { $r->has_request_body(\&post); } }; sub post { my $r = shift; if( $r->request_body ne "" && $r->request_body =~ /shop_id=([a-z0-9A-Z]+)/ ) { my $shop_id = "$1"; my $shard_id = find_shard($shop_id); if($shard_id ne "") { $r->internal_redirect("/shard" . $shard_id . $r->uri . "?" . $r->args); return; } } $r->internal_redirect("/shard_not_found"); }; sub find_shard { my $shop_id = shift; open(my $fh, "<", "/home/rails/nginx_shop_mapping.conf") or die("cant open filename"); while(my $row = <$fh>) { chomp $row; if($row ne "") { @parts = split / /, $row; if(@parts[0] eq $shop_id) { close $fh; return $parts[1]; } } } close $fh; return ""; } '; } location /shard0 { rewrite /shard0(.*) $1 break; proxy_pass http://api-0; proxy_redirect off; include /etc/nginx/api_proxy_params.conf; } location /shard1 { rewrite /shard1(.*) $1 break; proxy_pass http://api-1; proxy_redirect off; include /etc/nginx/api_proxy_params.conf; } location /shard_not_found { return 403; } }
Понятно, что тут много чего можно отрефакторить - я это сделаю когда-нибудь потом.
Happy sharding.