Как сделать content based шардинг в NGINX в зависимости от значения параметра в запросе
К нам пришла необходимость сделать свой шардинг с анализом содержимого запроса и 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.