Сравнительное нагрузочное тестирование Lua-коннекторов для Tarantool из NGINX

В последнее время на Хабре появляется достаточно много статей про Tarantool — базу данных и сервер приложений, который используется в Mail.Ru Group, Avito, Yota в разных интересных проектах. И вот, я подумал – а чем мы хуже? Давайте тоже попробуем.

В силу своей профессиональной деформации буду рассматривать следующий кейс:

  • Есть Web-ресурс, доступ к которому мы хотим ограничить;
  • Сам ресурс менять нельзя или крайне нежелательно.

Как подступиться к данной задаче?

Давайте поставим перед ресурсом шлюз, который будет проверять права пользователей, и в зависимости от результатов проверки пускать или не пускать пользователя на ресурс. Права доступа пользователей будем хранить в Tarantool. В нём есть master-master репликация, и, если нам нужно будет построить кластер из шлюзов, она придётся как нельзя кстати. В качестве основы для шлюза будем использовать NGINX (ну не писать же Web-сервер самим…).

Надо NGINX добавить «интеллекта», чтобы он понимал куда пользователю ходить можно, а куда — нельзя. Для этого можно было бы использовать ngx_http_auth_request_module, но непонятно как совместить это с Tarantool. Давайте последуем примеру OpenResty, и будем использовать для интеллектуализации нашего шлюза Lua, а именно lua-nginx-module.

Для обращения к Tarantool изнутри NGINX нам потребуется соответствующий драйвер, или «коннектор» для выбранного языка. Сами авторы Tarantool пишут, что если вам понадобилось иметь коннектор к Tarantool из Lua – то у вас что-то не то с архитектурой. Но в случае со связкой NGINX+Lua это может быть оправданно.

Гугление показывает наличие в природе аж трёх кандидатов:

  • github.com/ziontab/lua-nginx-tarantool — Реализован на nginx cosockets. Правда давно не обновлялся.
  • github.com/tarantool/tarantool-lua — официальный драйвер от разработчиков Tarantool. Является доработанным форком первого кандидата. Помимо nginx cosockets поддерживает обычные сокеты Lua.
  • github.com/perusio/lua-resty-tarantool — Уже два года без коммитов, нет поддержки семантики вызовов процедур Tarantool 1.7.

Что выбрать? Надо тестировать. Тем и займёмся.

Тестовый стенд:




От генератора нагрузки запросы передаются на шлюз в защищенном TLS виде (не забываем про проф. деформацию). Далее NGINX снимает TLS, и передаёт их защищаемому ресурсу в виде обычного HTTP.

Характеристики тестовых машин


Нагрузочная машина и защищаемый ресурс


(две одинаковые машины)
CPU 2xIntel Xeon E5 2680 @ 2.70GHz Sandy Bridge-EP/EX 32nm Technology 8 Cores/16 threads
RAM 32,0ГБ DDR3 @ 799MHz (11-11-11-28)
MB Supermicro X9DR3-F
Disk 223GB OCZ-VERTEX3 (SSD)
OS Debian 8.9 x64 (ядро 3.16.39-1)
NGINX 1.12.1
wrk 4.0.2-dirty [epoll] + GOST TLS patches

Шлюз

CPU 1 vCPU
RAM 8 Gb
Platform VMWare Workstation 12.5
Host CPU Intel Core i5 7600K 3.8 GHz
Host RAM 16 Gb
Host OS Windows 10 x64
NGINX 1.12.1
lua-nginx-module Latest master branch

Конфиг NGINX защищаемого ресурса


Ничего необычного — просто пустой GIF.

user  nginx;
worker_processes  32;

error_log  /var/log/ngate/nginx/error.log warn;
pid        /var/run/nginx.pid;

worker_rlimit_nofile 65535;

events {
    worker_connections  8192;
}

http {
    access_log  /var/log/ngate/nginx/access.log  main;
    keepalive_timeout  65;

    server {
	    listen       80;
	    server_name  fast-ipsec2-db8;

	    location / {
		root /var/www;
		index  index.html index.htm;
	    }

	    location = /ff/empty_gif.gif {
		empty_gif;
	    }
    } # end server
}

Конфиг NGINX шлюза:


worker_processes  1;

error_log  /var/log/nginx/error.log warn;

worker_rlimit_nofile 65535;

events {
    worker_connections  8192;
}

http {
    include       /etc/opt/nginx/mime.types;
    default_type  text/html;
    sendfile      on;
    keepalive_timeout  65;
    autoindex off;
    server_tokens off;

    lua_package_path '?.lua;/opt/lua/?.lua;';

    # HTTPS server
    server {
        listen       443 ssl;
        server_name  perf-test-1;

	ssl_certificate     www.example.com.crt;
	ssl_certificate_key www.example.com.key;
	ssl_protocols       TLSv1;
	ssl_ciphers         HIGH:!aNULL:!MD5;
    
        if ($request_method !~ ^(GET|HEAD|POST)$ )
        {
            return 444;
        }

        # Local Tarantool Node
        set $ng_local_tnt_addr  '127.0.0.1';
        set $ng_local_tnt_port  3320;

        # ff
        location /ff/ {
            proxy_pass http://fast-ipsec2-db8/ff/;
            access_by_lua_file /opt/lua/res_access.lua;
        }

    } # end server perf-test-1
} # end http

Обратите внимание на строчку:

access_by_lua_file /opt/lua/res_access.lua;

именно тут мы проверяем права пользователей.

Код /opt/lua/res_access.lua


Всё просто:

1. Извлекаем авторизационную куку;
2. Парсим запрос чтобы понять к какому ресурсу обращается пользователь;
3. Передаём полученные значения Tarantool, чтобы тот принял решение – пускать пользователя или нет.
4. Обрабатываем ответ Tarantool
5. В зависимости от ответа пускаем пользователя, или говорим «Access Denied».
Для простоты оставим за рамками статьи работу с правами доступа, и будем всегда пускать пользователя к ресурсу.

local auth_cookie_value = ngx.var.cookie_nginxauth
if auth_cookie_value == nil then
    ngx.log(ngx.WARN, "Authentication cookie not provided.")
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

local uri_root_regex = "(\\/[a-zA-Z0-9\\-\\._]+\\/)"
local m, err = ngx.re.match(ngx.var.uri, uri_root_regex, "ai")
if err then
    ngx.log(ngx.ERR, "Error in regexp: ", err)
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

if m == nil then
    ngx.log(ngx.ERR, "Regexp returned nil value.")
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

local uri_root = m[0]

if uri_root == nil then
    ngx.log(ngx.ERR, "error in regexp")
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

local tnt = require 'resty.tarantool'
#local tnt = require 'tarantool-lua.tarantool'

local tar, err = tnt:new({
    host = ngx.var.ng_local_tnt_addr,
    port = ngx.var.ng_local_tnt_port,
    --Default value 2000
    socket_timeout = 500,
#   connect_now = false,
})

if not tar:connect() then
    ngx.log(ngx.ERR, "TNT connection failed.")
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

local res, err = tar:call('check_access', {auth_cookie_value, uri_root})

if not tar:set_keepalive() then
    ngx.log(ngx.WARN, "TNT connection not set as keep-alive.")
end

if not res then
    ngx.log(ngx.ERR, "TNT call failed: " .. err)
    ngx.exit(ngx.HTTP_NOT_FOUND)
end

if res[1] ~= nil and res[1][1] == true then
    -- Access granted
    ngx.log(ngx.INFO, "Resource access granted: " .. uri_root)
    return
else
    ngx.log(ngx.ERR, "Resource access denied: " .. uri_root)
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

Код хранимой процедуры Tarantool


Так как авторизационная кука у нас всё равно игнорируется, не будем для простоты рассматривать процесс её получения и формирования.

local strict = require('strict')
strict.on()

function check_access(session_id, resource_name)
    if session_id == nil or resource_name == nil then
        return false
    end

    log.info('Access to resource ' .. resource_name .. ' granted.')
    return true
end

Методика тестирования


Для тестирования будем использовать утилиту wrk. Она хорошо зарекомендовала себя в нагрузочном тестировании и помимо TLS, поддерживает сценарии на Lua (хоть мы их и не будем использовать сейчас). Из особенностей – у wrk неотключаемый TLS Session Resumption, таким образом CPU шлюза не будет тратится на постоянные TLS-хендшейки. Чтобы проверить именно производительность проверок прав доступа в секунду, а не пропускную способность, будем запрашивать с защищаемого ресурса файл минимального размера – пустой GIF, занимающий 43 байта.

Приступим к тестированию.

Кандидат 1 (lua-nginx-tarantool):


Не работает совсем. Если использовать его согласно документации, то на


local tnt = require 'lua-nginx-tarantool.tarantool'
local tar, err = tnt:new({...})

Появляется ошибка:

runtime error: /opt/lua/res_access.lua:33: attempt to index local 'tnt' (a boolean value)

Разбираться не будем.

Кандидат 2 (tarantool-lua):


./wrk -t32 -c32 -d30s --latency --timeout 10s   -H "Host: perf-test-1" -H "Cookie:  nginxauth=XXX " https://192.168.85.159/ff/empty_gif.gif
 Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   252.12ms  248.32ms 514.22ms   31.13%
    Req/Sec     4.55      5.54    60.00     95.77%
  Latency Distribution
     50%   21.75ms
     75%  502.22ms
     90%  503.61ms
     99%  507.63ms
  3767 requests in 30.04s, 1.33MB read
  Non-2xx or 3xx responses: 1868
Requests/sec:    125.40
Transfer/sec:     45.49KB

Во-первых – мало. Всего 125 запросов в секунду.

Во-вторых – больше половины ответов – не ожидаемые 200, а что-то иное. Что же это? Ответ находится в error_log NGIXN:

[error] 11856#0: *23410 [lua] res_access.lua:42: TNT connection failed.

Что-то идёт не так – то ли Tarantool отвергает соединения, то ли NGINX не может их переварить.
Но попробуем дальше.

Кандидат 3 (lua-resty-tarantool):


./wrk -t32 -c32 -d30s --latency --timeout 10s   -H "Host: perf-test-1" -H "Cookie:  nginxauth=XXX " https://192.168.85.159/ff/empty_gif.gif
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.03ms    1.13ms  28.79ms   79.90%
    Req/Sec   110.13     10.43   131.00     75.49%
  Latency Distribution
     50%    8.63ms
     75%    9.46ms
     90%   10.64ms
     99%   11.81ms
  105344 requests in 30.03s, 43.40MB read
Requests/sec:   3508.50
Transfer/sec:      1.45MB

Ого! 3500 запросов в секунду, и ни единой ошибки!
И в error_log NGINX – тишина.

Выводы


Несмотря на почтенный возраст, и некоторые недочёты Кандидат 3 (lua-resty-tarantool) явно является не просто лидером, а единственным вариантом использования в production. И ещё раз мы убедились в необходимости тестирования различных вариантов использования перед тем как принимать решение о использовании или не использовании той или иной технологии в реальных проектах.
Метки:
  • +15
  • 3,4k
  • 3
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 3
  • +1
    Я так понял, вы на каждый запрос к веб-серверу открываете новый коннект к тарантулу чтобы сделать один простой запрос? Я плохо знаком с nginx-расширениями, но нельзя ли держать постоянное подключение к тарантулу и через него слать запросы?

    3500 rps это как-то очень и очень мало.
    • +1
      1. Обратите внимание на фрагмент:
      if not tar:set_keepalive() then
          ngx.log(ngx.WARN, "TNT connection not set as keep-alive.")
      end

      Метод set_keepalive() как раз и возращает соединение в пул NGINX. И оно может быть повторно использованно другим запросом.

      2. Казалось бы да, 3500 RPS это не много, но не забываем что на тестовой машине было задействовано всего одно ядро. И один worker-процесс NGINX (которому ещё и TLS делать надо) делил CPU с Тарантулом.
      Если выделить NGINX побольше ядер (например 30), и освободить как минимум одно ядро для Тарантула, то можно получить существенно более радостную картину:
      Requests/sec: 54746.21
      *nginx грузит ~5-87% CPU
      *Tarantool грузит ~135% CPU
    • +3
      Возьмите github.com/tarantool/nginx_upstream_module.

      Именно его принято использовать при построении сервисов на базе тарантула и nginx-а. И главное — появится возможность управлять конектами к тарантулам в секции upstream и не писать все это самому, ведь в продакшене вы наверняка захотите работать с пачкой тарантулов, чтобы было распределение нагрузки между ними, failover и прочие фишки, которые есть у nginx upstream-ов.

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