Технологии безопасности

Сайт посвященный вопросам безопасности

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

(adsbygoogle = window.adsbygoogle || []).push({});

В последнее время на Хабре появляется достаточно много статей про 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. И ещё раз мы убедились в необходимости тестирования различных вариантов использования перед тем как принимать решение о использовании или не использовании той или иной технологии в реальных проектах.