В последнее время на Хабре появляется достаточно много статей про Tarantool — базу данных и сервер приложений, который используется в Mail.Ru Group, Avito, Yota в разных интересных проектах. И вот, я подумал – а чем мы хуже? Давайте тоже попробуем.
В силу своей профессиональной деформации буду рассматривать следующий кейс:
Как подступиться к данной задаче?
Давайте поставим перед ресурсом шлюз, который будет проверять права пользователей, и в зависимости от результатов проверки пускать или не пускать пользователя на ресурс. Права доступа пользователей будем хранить в Tarantool. В нём есть master-master репликация, и, если нам нужно будет построить кластер из шлюзов, она придётся как нельзя кстати. В качестве основы для шлюза будем использовать NGINX (ну не писать же Web-сервер самим…).
Надо NGINX добавить «интеллекта», чтобы он понимал куда пользователю ходить можно, а куда — нельзя. Для этого можно было бы использовать ngx_http_auth_request_module, но непонятно как совместить это с Tarantool. Давайте последуем примеру OpenResty, и будем использовать для интеллектуализации нашего шлюза Lua, а именно lua-nginx-module.
Для обращения к Tarantool изнутри NGINX нам потребуется соответствующий драйвер, или «коннектор» для выбранного языка. Сами авторы Tarantool пишут, что если вам понадобилось иметь коннектор к Tarantool из Lua – то у вас что-то не то с архитектурой. Но в случае со связкой NGINX+Lua это может быть оправданно.
Гугление показывает наличие в природе аж трёх кандидатов:
Что выбрать? Надо тестировать. Тем и займёмся.

От генератора нагрузки запросы передаются на шлюз в защищенном TLS виде (не забываем про проф. деформацию). Далее NGINX снимает TLS, и передаёт их защищаемому ресурсу в виде обычного HTTP.
(две одинаковые машины)
Ничего необычного — просто пустой GIF.
Обратите внимание на строчку:
именно тут мы проверяем права пользователей.
Всё просто:
1. Извлекаем авторизационную куку;
2. Парсим запрос чтобы понять к какому ресурсу обращается пользователь;
3. Передаём полученные значения Tarantool, чтобы тот принял решение – пускать пользователя или нет.
4. Обрабатываем ответ Tarantool
5. В зависимости от ответа пускаем пользователя, или говорим «Access Denied».
Для простоты оставим за рамками статьи работу с правами доступа, и будем всегда пускать пользователя к ресурсу.
Так как авторизационная кука у нас всё равно игнорируется, не будем для простоты рассматривать процесс её получения и формирования.
Для тестирования будем использовать утилиту wrk. Она хорошо зарекомендовала себя в нагрузочном тестировании и помимо TLS, поддерживает сценарии на Lua (хоть мы их и не будем использовать сейчас). Из особенностей – у wrk неотключаемый TLS Session Resumption, таким образом CPU шлюза не будет тратится на постоянные TLS-хендшейки. Чтобы проверить именно производительность проверок прав доступа в секунду, а не пропускную способность, будем запрашивать с защищаемого ресурса файл минимального размера – пустой GIF, занимающий 43 байта.
Приступим к тестированию.
Не работает совсем. Если использовать его согласно документации, то на
Появляется ошибка:
Разбираться не будем.
Во-первых – мало. Всего 125 запросов в секунду.
Во-вторых – больше половины ответов – не ожидаемые 200, а что-то иное. Что же это? Ответ находится в error_log NGIXN:
Что-то идёт не так – то ли Tarantool отвергает соединения, то ли NGINX не может их переварить.
Но попробуем дальше.
Ого! 3500 запросов в секунду, и ни единой ошибки!
И в error_log NGINX – тишина.
Несмотря на почтенный возраст, и некоторые недочёты Кандидат 3 (lua-resty-tarantool) явно является не просто лидером, а единственным вариантом использования в production. И ещё раз мы убедились в необходимости тестирования различных вариантов использования перед тем как принимать решение о использовании или не использовании той или иной технологии в реальных проектах.
В силу своей профессиональной деформации буду рассматривать следующий кейс:
- Есть 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. И ещё раз мы убедились в необходимости тестирования различных вариантов использования перед тем как принимать решение о использовании или не использовании той или иной технологии в реальных проектах.