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