Введение
В последнее время на конференциях и в сети размещается не мало статей по работе с платформой Tarantool от Mail.ru Group. И недавное мне подвернулась задача где я смог его попробывать.
Задача заключалось в том, что получая телеметрию от транспортных средств мне нужно было хранить посление точки от каждой машины и отдавать их через REST API, для оперативного отображения на карте при обращении клиента.
Так как делать постоянные обновления записей (около 600 операций в секунду) в осносвной СУБД PosgtreSQL трудоемко (по причине MVCC и vauucum не успевает отрабатывать), я решил попробывать Tarantool для этой задачи, так как он имеет высокую скорость обновления и его можно сделать персистентым, что для меня было критическим условием.
Кроме того Tarantool позиционирует себя не только как хранилище, но также как и сервер приложений, что позволяет обрабаывать данные там же где они хранятся.
Все ниже описанные действия я производил с Tarantool 1.10
Особенности приложений tarantool
Первая особенность с которой я столкнулся это необходимость создания рабочей дирректории (параметр work_dir
) для приложения tarantool, а также установки пользователей для приложения. Если этого не сделать приложения отлаживать крайне трудно и не понятно в каком состоянии находится хранилище.
Следующий ньюанс был связан с производительностью Go-шного драйвера go-tarantool, так как при дефолтных настройках он работал крайне медленно, и после долгого чтения документации нужной произвоительности я не добился, то я решил реализовать добовление точек в хранилище через endpoint, чтобы платформа сама управляла этими операциями без посредников, и это сразу решило проблему.
Написание приложения
Приложения для платформы tarantool пишутся на языках Lua и C. В моем случае я использовал Lua.
Исходя из описанного выше, приложение должно выпорлнять следующие действия:
- отдавать список точек по ТС (или конретному ТС)
- обновлять информацию по последней точке от ТС
Итак, первое что нужно сделать это создать конфигурацию приложения:
#!/usr/bin/env tarantool
box.cfg {
listen = '*:6432';
log_level = 7;
work_dir = "/opt/tarantool/server"
}
Тут я задаю порт приложения для внешнего соедения (например из Go-приложения), уровень логирования и рабочую дирректорию.
Далее необходимо иниицализовать рабочее пространство нашего приложения:
box.once('init', function()
box.schema.space.create('lastpoints', { if_not_exists = true, engine = 'vinyl' })
box.space.lastpoints:format({
{ name = 'tracker_code', type = 'unsigned' },
{ name = 'lon', type = 'number' },
{ name = 'lat', type = 'number' },
{ name = 'point_id', type = 'unsigned' },
{ name = 'navigate_date', type = 'unsigned' },
{ name = 'course', type = 'unsigned' },
})
box.space.lastpoints:create_index('primary', { type = 'tree', parts = { 'tracker_code' } })
box.space.lastpoints:create_index('secondary', { unique = false, type = 'tree', parts = { 'navigate_date' } })
box.schema.user.grant('guest', 'read,write,execute', 'universe')
box.schema.user.passwd('admin', 'admin')
end)
В этом коде создается пространство lastpoints
с заданными полями, а также ряд индексов к ней:
- primary - отвечает за уникальность кода устройства, от которого пришла точка
- secondary - строит дерево по датам формирования точки, для быстрого поиска
Так как Tarantool по сути является key-value хранилищим, то индекс primary
будет соответствовать уникальному ключу записи.
Также нужно важно указать engine = 'vinyl'
, если нужна персистентность данных, т. к. по-умолчанию tarantool хранит все данные в оперативной памяти. Подробнее можно прочитать в документации.
Вначале создадим функцию для добавления точек:
local function add_point_handler(req)
local point = req:json()
log.debug("Received point")
log.debug(point)
box.space.lastpoints:put {
point.tracker_code,
point.lon,
point.lat,
point.point_id,
point.navigate_date,
point.course,
}
log.debug("Point saved success")
local resp = req:render{text=""}
resp.status = 201
return resp
end
Код довольно простой: он получает json определенного формата в теле запроса и укладывает его в пространосто lastpoints
, которое было создано на этапе инициализации. Кроме того используется библиотека log
для логирования процесса добавления точки.
Теперь нужен enpdoint через который мы сможем посмотреть добавленные точки. Он должен обладать следующими параметрами:
- client - возвращает точку по конкретному устройству
- delta - возвращает массив точек за интервал размером delta секунд
- без параметров должны вернуться все точки, находящие в базе
Функция будет выглядеть так:
local function handler(req)
local client = tonumber(req:param('client'))
local delta = tonumber(req:param('delta'))
local table_last_point = box.space.lastpoints
local result = {}
if client then
local rec = table_last_point:get(client)
if delta then
if rec.navigate_date > os.time() + delta then
table.insert(result, rec)
end
else
table.insert(result, rec)
end
else
for _, rec in box.space.lastpoints.index.secondary:pairs(delta, { iterator = 'GE' }) do
table.insert(result, rec)
end
end
return req:render { json = result }
end
В ней сналача проверяется задан ли код устройства и если он задан, то выбирается записть с ключом этого устрайства, а дальше она проверяется на попадание в интревал (если он задан). Если код устройства не задан, то происхрдит поиск записей у которых вторичный индекс (secondary) удовлетворяет заданному интервалу.
Когда функции реализованы, осталось подключить библиотеку http для реализации web сервера и роутинга:
#!/usr/bin/env tarantool
box.cfg {
listen = '*:6432';
log_level = 7;
work_dir = "/opt/tarantool/server"
}
log = require('log')
box.once('init', function()
box.schema.space.create('lastpoints', { if_not_exists = true, engine = 'vinyl' })
box.space.lastpoints:format({
{ name = 'tracker_code', type = 'unsigned' },
{ name = 'lon', type = 'number' },
{ name = 'lat', type = 'number' },
{ name = 'point_id', type = 'unsigned' },
{ name = 'navigate_date', type = 'unsigned' },
{ name = 'course', type = 'unsigned' },
})
box.space.lastpoints:create_index('primary', { type = 'tree', parts = { 'bnso_code' } })
box.space.lastpoints:create_index('secondary', { unique = false, type = 'tree', parts = { 'navigate_date' } })
box.schema.user.grant('guest', 'read,write,execute', 'universe')
box.schema.user.passwd('admin', 'admin')
end)
local function handler(req)
local client = tonumber(req:param('client'))
local delta = tonumber(req:param('delta'))
local table_last_point = box.space.lastpoints
local result = {}
if client then
local rec = table_last_point:get(client)
if delta then
if rec.navigate_date > os.time() + delta then
table.insert(result, rec)
end
else
table.insert(result, rec)
end
else
for _, rec in box.space.lastpoints.index.secondary:pairs(delta, { iterator = 'GE' }) do
table.insert(result, rec)
end
end
return req:render { json = result }
end
local function add_point_handler(req)
local point = req:json()
log.debug("Received point")
log.debug(point)
box.space.lastpoints:put {
point.tracker_code,
point.lon,
point.lat,
point.point_id,
point.navigate_date,
point.course,
}
log.debug("Point saved success")
local resp = req:render{text=""}
resp.status = 201
return resp
end
local server = require('http.server').new(nil, 8080)
server:route({ path = '/', method = 'GET' }, handler)
server:route({ path = '/', method = 'POST' }, add_point_handler)
server:start()
Заключение
Создать приложений на Tarantool действительно просто, но вот сторонние драйвера для работы с ним мне показались сыроватыми. Тут есть несколько вариантов действий или обходное решение (как в моем случае) или же скурпулезно изучать документацию и крутить всевозможные настройки которых предостаточно.
В целом впечатления у меня остались положительные и если еще подвернется возможность буду его использовать в дальнейшем, тем более что у него предостаточно библиотек, даже ечть специальная библиотека gis
для работы с геоданными.