Студия Михаила Кечинова

ruen
Навигация
Главная Как сделать content based шардинг в NGINX в зависимости от значения параметра в запросе

Как сделать content based шардинг в NGINX в зависимости от значения параметра в запросе

К нам пришла необходимость сделать свой шардинг с анализом содержимого запроса и proxy_pass. Логика нашей системы такова, что мы можем распределить отдельные части БД и обработчиков API по разным серверам и по входящим запросам от клиентов перенаправлять запрос на соответствующий сервер в зависимости от идентификатора проекта. Выглядеть это должно примерно так:

Исходные условия следующие:

  • база легко бьется по идентификатору проекта;
  • один клиент имеет от 1 до бесконечности проектов;
  • каждый запрос сопровождается параметром uniqid, являющимся уникальным идентификатором проекта и выглядящим как 18263IUGF1I7FTI716R1R;
  • запросы приходят как GET, так и POST;
  • количество данных в запросе минимально - максимум 1Кб.

Задача: настроить NGINX так, чтобы по содержимому поля uniqid перенаправлять запрос на соответствующий проекту сервер.

Ограничения:

  1. Обойтись минимальными изменениями NGINX.
  2. Тиражировать решение нужно быстро.
  3. Скорость обработки запросов не должна возрасти значительно, т.к. наше требование к скорости обработки — 50ms/запрос.

Если у вас нет времени на изучение неработающих способов, прокручивайте сразу до заголовка “Готовое решение” и копируйте.

Итак.

Первое, что напрашивается – директива 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.