Изучаем Tarantool + Lua

в 6:42, , рубрики: Lua, nosql, tarantool, Блог компании Mail.Ru Group, метки: ,

logo
Хочу поделиться опытом изучения Tarantool. Про все преимущества и особенности самого Тарантула я писать не буду, на эту тему было много статей (например, эта, эта и эта). Этот пост рассказывает о том, как начать работать с тарантулом и про некоторые особенности и вкусности которые можно получить из коробки.

Устанавливаем

Собственно, первое, с чем я столкнулся — это установка. Так как установить для тестов мне его надо было на MacOS, то, скорее всего, на это мало кто наступит, но тем не менее. Пакет, который предлагали на сайте, не установился то ли из-за каких то зависимостей, то ли из-за того, что система уже пережила не один эксперимент. Поэтому решил собирать из исходников.

Процесс установки хорошо описан в README. Не забываем выкачивать субмодули, если исходники выкачивались из git-а. Еще при сборке под MacOS не нужно пугаться того, что не все тесты проходят — в документации написано, что это нормально.
Если хотите получить консольный клиент, cmake надо запускать с ключом DENABLE_CLIENT=true. Собственно, после make мы получаем сервер и клиент, если его попросили, src/box/tarantool_box и client/tarantool/tarantool соответственно.

Конфигурируем сервер

За пример можно взять конфигурацию одного из тестов, например test/box/tarantool.cfg
Одним из важных параметров является slab_alloc_arena — это объем памяти, используемой Тарантулом. Советую более детально изучить этот параметр. Так же стоит обратить внимание на rows_per_wal, чтобы потом не удивляться, отчего так много маленьких файликов лежит )))
Теперь приступаем к самому интересному. Тарантулу необходимо знать только про индексы, и ему абсолютно всё равно, что будет в тупле и какого размера он будет. Собственно, в конфиге описываем только индексы. Более подробно типы индексов можно изучить в документации. Из основного: при выборе индекса нужно точно понимать, для чего он нужен. HASH-индекс не может быть неуникальным. TREE-индексы хорошо использовать для организации сортированного списка по неуникальным значениям. Также индексы могут быть составными. Индексы описываются для каждого спейса. Спейсов может быть много.
Итого: представим, что нам нужен спейс с 5 полями. Первое поле неуникальное, при этом первое + второе поле уникальные; по ним будем делать точечные селекты. В четвертом поле лежит некий параметр для сортировки. Итого строим уникальный индекс:

space[0].index[0].type = «HASH» # тип индекса
space[0].index[0].unique = 1 # признак уникальности
space[0].index[0].key_field[0].fieldno = 0 #номер записи в тупле
space[0].index[0].key_field[0].type = «NUM» # тип данных
space[0].index[0].key_field[1].fieldno = 1
space[0].index[0].key_field[1].type = «NUM»

Строим индекс, для выборки пачки неуникальных записей по первому полю:

space[0].index[1].type = «TREE»
space[0].index[1].unique = 0
space[0].index[1].key_field[0].fieldno = 0
space[0].index[1].key_field[0].type = «NUM»

Строим индекс для выборки отсортированных записей по первому+третьему полю

space[0].index[2].type = «TREE»
space[0].index[2].unique = 0
space[0].index[2].key_field[0].fieldno = 0
space[0].index[2].key_field[0].type = «NUM»
space[0].index[2].key_field[1].fieldno = 3
space[0].index[2].key_field[1].type = «NUM»

Ну и не забываем, что спейс нужно сделать активным:

space[0].enabled = 1

Теперь опишем еще один спейс, где будут храниться туплы из двух полей: первое —уникальное, второе — нет. Типичное key value-хранилище в своём самом простом представлении:

space[1].enabled = 1
space[1].index[0].type = «HASH»
space[1].index[0].unique = 1
space[1].index[0].key_field[0].fieldno = 0
space[1].index[0].key_field[0].type = «NUM»

Собственно, с этими настройками Тарантул готов к работе. Нужно проинициализировать хранилище — и в путь.

> ./src/box/tarantool_box --init-storage
tarantool/src/box/tarantool_box: space 0 successfully configured
tarantool/src/box/tarantool_box: space 1 successfully configured
tarantool/src/box/tarantool_box: creating './00000000000000000001.snap.inprogress'
tarantool/src/box/tarantool_box: saving snapshot './00000000000000000001.snap'
tarantool/src/box/tarantool_box: done

Как видим, все файлы он создал в той папке, откуда мы запустили Тарантул. Если вы хотите ее изменить, то в настройках есть параметр wirk_dir, который можно определить по своему желанию.

После этого запускаем сервер:

> ./src/box/tarantool_box --background
> ps xa | grep tarantool
 5627   ??  Us     0:10.55 tarantool/src/box/tarantool_box --background

Ура! Можно приступать к наполнению данными и извлечению их в нужной последовательности и по нужным критериям.

Начинаем работу

Как пользоваться консольным клиентом и детально расписывать каждую команду я сейчас не буду — это могло бы быть темой отдельной статьи. А сейчас я остановлюсь подробнее на процедурах на Lua. Одна из интересных, на мой взгляд, возможностей — это встроенные процедуры. При их помощи можно сделать некоторый «черный ящик» с бизнес-логикой, которую легко и независимо от остального кода можно поменять, тем самым отделяя техническую часть от бизнес-модели. Думаю, что Lua это очень неплохая подсказка тому, что там, будет храниться бизнес логика.
Итак, тарантул при старте пытается загрузить файл init.lua, в который мы и будем складывать наши функции.

Особое внимание при написании функций нужно уделить типам данных, которых как бы нет в Lua и, например, нет в Perl, но из-за особенностей реализации протоколов чиселки из Perl в Lua приходят совсем не так, как они приходят из консольного клиента. Так что, пока не поддержали типы данных в адаптерах к Тарантулу, можно передавать всегда строки и там где нужно их конвертировать в числа.

Напишем процедуру, которая будет изменять одно поле в зависимости от значения другого поля и текущей даты. Очень стандартная задача, которая, как правило, «замыливается» в коде, и при очередном рефакторинге происходит ошибка с расчётом дат в этой логике.

function increase_score(id, id2)
        local id = tonumber(id) — переводим строки в числа
        local fid = tonumber(id2)
        if(id == nill or fid == nil) then — проверяем входящие параметры
                return false
        end
        -- получаем дату в структуре date_time
        local dt = os.date("*t") 
        -- получаем таймстемп на начало текущего дня
        local cd = os.time({year = dt.year; month=dt.month; day=dt.day}) 
        -- получаем запись из 0 спейса, поиск производим по 0 индексу
        local tup = box.select('0','0', id, fid) 
        if( tup == nil ) then 
                -- если такой записи еще не было, то создадим ее
                box.insert('0', id, fid, cd, 100,cd) 
        else
                local lu =  box.unpack('i', tup[2])
                local sc =  box.unpack('i', tup[3])
                local la =  box.unpack('i', tup[4])
                -- получим разницу дней прошедших с последнего обновления
                local diffs = (math.floor(sc/((la-lu)/24/60/60+30))+1)*((cd-lu)/24/60/60) 
                if( diffs < 0 or diffs > sc ) then 
                -- подстрахуемся от того, что дата последнего обновления:
                -- - не более 30 дней назад 
                -- - при делении с округлением и умножении мы не превысили исходное значение 
                        diffs = sc
                end
                -- добавим константу по условиям задачи 
                diffs = 100 - diffs 
                box.update('0',{id;fid},'+p=p=p',3, diffs,2,cd,4,cd)
                -- обновим запись в 0 спейсе по первичному ключу
        end
end

Итого можно считать, что это атомарное действие с точки зрения внешней системы.

Теперь несколько слов про производительность. Частота вызова этой процедуры достигла 20000 rps. При этом загруженность камушка была 67%.

Далее расскажу про еще одну вкусность. Обновление с расчетом, описанное выше — это хорошо, но кроме обновления, как правило в задаче говориться о том, что эти данные надо доставать и более того отдавать их надо в отсортированном порядке. Чтобы не выносить сортировку во внешнюю систему и не делать сортировку самим, воспользуемся индексами.

function get_top(uid)
        local id = tonumber(uid) — переводим в чиселку
        if(id == nil) then  -- проверяем, что входящий параметр в порядке
                return false    
        end                     
        ret = {}
        -- создаём итератор по индексу который будет идти в порядке убывания начиная с id. 
        -- Особенность этого итератора в том, что можно указывать только одно из значений полей
        -- используемых в индексе, не указанные могут иметь любые значения. 
        -- Но надо учитывать, что итератор может выйти за пределы переданных параметров, 
        -- а могут быть и меньше (или больше, зависит от типа итератора)
        for v in box.space[0].index[2]:iterator(box.index.LE, id) do 
                if( v == nil or #ret == 10 or box.unpack('i',v[0]) ~= id) then break end 
                -- проверяем, что:
                -- - индекс не закончился, 
                -- - мы не набрали 10 нужных нам записей
                -- - мы еще не вышли за пределы указанного параметра
                table.insert(ret, v)
        end
        return unpack(ret)
end

Так мы создали процедуру, которая, пользуясь прелестями индекса, возвращает нам отсортированный в порядке убывания список туплов. По поводу порядка сортировки можно поискать в документации box.index.LE и посмотреть, что чем отличается и как это работает.

Ну и самое главное: из клиента эти процедуры вызываются так:

> lua increase_score(1,2)

и

> lua get_top(1)

В очередной статье напишу, как всем этим добром можно решить одну из распространённых задач и покажу особенности использования драйвера для общения с Тарантулом из Perl.

Автор: shulyakovskiy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js