На Хабре уже опубликовано множество статей о платформе Tarantool. Например, есть обзорные материалы о создании key-value хранилищ, но они редко углубляются в детали реализации. Также доступны практические примеры, такие как реализация key-value хранилища на Tarantool 2.x с использованием фреймворка Cartridge и Docker Compose. Однако эти примеры не раскрывают внутренней логики работы приложения.
Цель этой статьи — продемонстрировать процесс создания простого key-value хранилища на основе актуальной версии Tarantool 3.x, а также показать, как его собрать и развернуть.
Исходный код проекта доступен в репозитории.
Оглавление
Функциональные требования
Запись и обновление пар «ключ-значение».
Установка срока жизни (TTL) для записей.
Поиск значения по точному совпадению ключа.
Поиск записей по префиксу ключа.
Нефункциональные требования
Реализация на Tarantool 3.x.
Поддержка шардинга.
Предоставление метрик в формате Prometheus text-based exposition.
Поставка в виде Docker-образа и развертывание с помощью Docker Compose.
1. Общая информация о Tarantool
Tarantool — это платформа для вычислений, объединяющая встроенную базу данных и сервер приложений на языке Lua. Она включает модули, такие как:
box — работа с данными.
fiber — управление легковесными потоками для асинхронных задач.
http — HTTP-клиент и сервер.
С помощью пакетного менеджера LuaRocks (форк от команды Tarantool: github.com/tarantool/luarocks) можно подключать сторонние модули, например:
vshard — организация шардинга.
crud — упрощение CRUD-операций в кластере vshard.
expirationd — автоматическое удаление устаревших данных.
Модули для работы с MySQL (github.com/tarantool/mysql) и PostgreSQL (github.com/tarantool/pg).
metrics-export-role — сбор метрик для Tarantool 3.x.
1.1. Организация хранения данных
Модуль box отвечает за работу с базой данных. Данные хранятся в spaces — аналогах таблиц в SQL. Spaces содержат tuples — записи базы данных.
Атрибуты space:
Уникальное имя, задаваемое пользователем.
Уникальный числовой идентификатор (автоматический или пользовательский).
Движок (engine): memtx (in-memory) или vinyl (on-disk для больших объемов данных).
Для space можно задавать первичный и вторичные индексы. Схема данных в Tarantool 2.x и 3.x определяется программно через Lua-скрипты. Для миграций схемы в кластере Cartridge (Tarantool 2.x) используется модуль migrations.
Пример создания space и первичного индекса:
box.schema.create_space('key_value', {
format = {
{ name = 'key', type = 'string' },
{ name = 'value', type = 'string' }
},
if_not_exists = true
})
box.space.key_value:create_index('id', {
type = 'tree',
parts = { 'key' },
unique = true,
if_not_exists = true
})
1.2. Шардирование данных. Кластеры vshard
Для шардинга используется модуль vshard, поддерживающий Tarantool 2.x и 3.x. Он применяется и в БД picodata. Tuples делятся на виртуальные сегменты (buckets), которые распределяются между шардами или наборами реплик (replicasets).
Для шардинга нужен индекс (shard_index), по умолчанию — bucket_id. Его имя можно изменить в настройках vshard.
Пример создания space с индексами для шардинга:
box.schema.create_space('key_value', {
format = {
{ name = 'key', type = 'string' },
{ name = 'bucket_id', type = 'unsigned' },
{ name = 'value', type = 'string' }
},
if_not_exists = true
})
box.space.key_value:create_index('id', {
type = 'tree',
parts = { 'key' },
unique = true,
if_not_exists = true
})
box.space.key_value:create_index('bucket_id', {
type = 'tree',
parts = { 'bucket_id' },
unique = false,
if_not_exists = true
})
Роли в кластере vshard:
storage: хранение buckets. В replicaset один экземпляр — мастер (чтение и запись), остальные — реплики (только чтение).
router: маршрутизация запросов. Есть экспериментальный Go VShard Router.
rebalancer: равномерное распределение buckets (может назначаться автоматически).
1.3. Средства разработки
Для создания приложений и управления экземплярами в Tarantool 2.x применялась утилита Cartridge CLI. В Tarantool 3.x используется новая CLI — tt.
Примеры конфигураций на основе tt:
Одиночная БД: create_db.
Кластер с vshard: sharded_cluster.
С vshard и crud: sharded_cluster_crud.
С vshard, crud, и метриками: sharded_cluster_crud_metrics.
2. Реализация key-value хранилища
2.1. Настройка окружения и создание каркаса проекта
Установим Tarantool и tt
. Официальные пакеты доступны для nix-систем, а для Windows — через WSL. Инструкции для Ubuntu:
curl -L https://tarantool.io/repository/3/installer.sh | bash
sudo apt-get install -y tt tarantool
Создадим каркаc проекта на основе шаблона vshard_cluster:
tt create cluster-app \
--name tt_kv \
-d ${PWD} \
-f \
-s \
--var bucket_count=100 \
--var replicasets_count=1 \
--var replicas_count=2 \
--var roles_count=1
Команда создаст директорию tt_kv с файлами:
config.yaml: конфигурация кластера.
instances.yml: описание экземпляров.
router.lua: скрипт для router.
storage.lua: скрипт для storage.
tt_kv-scm-.rockspec*: конфигурация зависимостей.
2.2. Обновление зависимостей
Обновим файл tt_kv-scm-1.rockspec
, добавив актуальные модули:
package = 'tt_kv'
version = 'scm-1'
source = {
url = '/dev/null',
}
dependencies = {
'crud == 1.5.2-1',
'expirationd == 1.6.1-1',
'metrics-export-role == 1.0.0-1',
'vshard == 0.1.34-1'
}
build = {
type = 'none'
}
Добавлены:
crud: упрощение работы с данными.
expirationd: удаление записей по TTL.
metrics-export-role: метрики для Prometheus.
2.3. Настройка экземпляров storage
Экземпляры storage хранят данные и реализуют логику работы с хранилищем. Они вызываются через router с использованием учетной записи storage, которой нужны права на выполнение функций crud.
2.3.1. Определение схемы данных
Создадим space key_value
с полями:
key (string): ключ.
bucket_id (unsigned): идентификатор для шардинга.
value (string): значение.
expire_at (unsigned): время истечения TTL.
Индексы:
id: первичный, по key (уникальный, tree).
bucket_id: для шардинга (неуникальный, tree).
expire_at_idx: для TTL (неуникальный, tree).
box.schema.create_space('key_value', {
format = {
{ name = 'key', type = 'string' },
{ name = 'bucket_id', type = 'unsigned' },
{ name = 'value', type = 'string' },
{ name = 'expire_at', type = 'unsigned' }
},
if_not_exists = true
})
box.space.key_value:create_index('id', {
type = 'tree',
parts = { 'key' },
unique = true,
if_not_exists = true
})
box.space.key_value:create_index('bucket_id', {
type = 'tree',
parts = { 'bucket_id' },
unique = false,
if_not_exists = true
})
box.space.key_value:create_index('expire_at_idx', {
type = 'tree',
parts = { 'expire_at' },
unique = false,
if_not_exists = true
})
2.3.2. Удаление записей с истекшим TTL
Для автоматического удаления используем expirationd. Функция проверяет, истек ли срок записи (expire_at > 0 и текущее время > expire_at):
local function is_expired(args, tuple)
return (tuple[4] > 0) and (require('fiber').time() > tuple[4])
end
2.3.3. Поиск по префиксу ключа
Функция get_by_prefix_locally
выполняет поиск на каждом replicaset:
local function get_by_prefix_locally(prefix)
local result = {}
local index = box.space.key_value.index.id
local iter = index:iterator('GE', { prefix })
for tuple in iter do
local key = tuple[1]
if string.sub(key, 1, #prefix) == prefix then
table.insert(result, {
key = key,
value = tuple[3],
expire_at = tuple[4]
})
else
break
end
end
return result
end
2.4. Настройка router
Router маршрутизирует запросы к шардам. Для поиска по префиксу используем функцию get_by_prefix_locally
через crud:
local function get_by_prefix(prefix)
local result, err = crud.map_call('key_value.get_by_prefix_locally', {prefix})
if not result then
return nil, "Error during map_call: " .. tostring(err)
end
return result.data
end
2.5. Настройка кластера
Настройка выполняется в config.yaml
.
2.5.1. Учетные записи
Создаем роль crud-role и учетную запись app:
config:
context:
app_user_password:
from: env
env: APP_USER_PASSWORD
client_user_password:
from: env
env: CLIENT_USER_PASSWORD
replicator_user_password:
from: env
env: REPLICATOR_USER_PASSWORD
storage_user_password:
from: env
env: STORAGE_USER_PASSWORD
credentials:
roles:
crud-role:
privileges:
- permissions: [ "execute" ]
lua_call: [ "crud.delete", "crud.get", "crud.upsert" ]
users:
app:
password: '{{ context.app_user_password }}'
roles: [ public, crud-role ]
client:
password: '{{ context.client_user_password }}'
roles: [ super ]
replicator:
password: '{{ context.replicator_user_password }}'
roles: [ replication ]
storage:
password: '{{ context.storage_user_password }}'
roles: [ sharding ]
2.5.2. Роль storage
Добавляем роли crud-storage, expirationd, metrics-export:
groups:
storages:
roles:
- roles.crud-storage
- roles.expirationd
- roles.metrics-export
roles_cfg:
roles.expirationd:
cfg:
metrics: true
key_value_task:
space: key_value
is_expired: key_value.is_expired
options:
atomic_iteration: true
force: true
index: 'expire_at_idx'
iterator_type: GT
start_key:
- 0
tuples_per_iteration: 10000
replication:
failover: election
database:
use_mvcc_engine: true
replicasets:
storage-001:
instances:
storage-001-a:
roles_cfg:
roles.metrics-export:
http:
- listen: '0.0.0.0:8081'
endpoints:
- path: /metrics/prometheus/
format: prometheus
iproto:
listen:
- uri: 127.0.0.1:3301
advertise:
client: 127.0.0.1:3301
storage-001-b:
roles_cfg:
roles.metrics-export:
http:
- listen: '0.0.0.0:8082'
endpoints:
- path: /metrics/prometheus/
format: prometheus
iproto:
listen:
- uri: 127.0.0.1:3302
advertise:
client: 127.0.0.1:3302
2.5.3. Роль router
Добавляем роли crud-router и metrics-export:
groups:
routers:
roles:
- roles.crud-router
- roles.metrics-export
roles_cfg:
roles.crud-router:
stats: true
stats_driver: metrics
stats_quantiles: true
app:
module: router
sharding:
roles: [ router ]
replicasets:
router-001:
instances:
router-001-a:
roles_cfg:
roles.metrics-export:
http:
- listen: '0.0.0.0:8083'
endpoints:
- path: /metrics/prometheus/
format: prometheus
iproto:
listen:
- uri: 127.0.0.1:3303
advertise:
client: 127.0.0.1:3303
3. Развертывание хранилища
Создаем Docker-образ на основе tarantool/tarantool
:
FROM tarantool/tarantool:3.2.0
# Install dependencies
RUN apt-get update && \
apt-get install -y git unzip cmake tt
# Initialize tt structure
RUN tt init && \
mkdir tt_kv && \
ln -sfn ${PWD}/tt_kv/ ${PWD}/instances.enabled/tt_kv
# Copy cluster configs
COPY tt_kv /opt/tarantool/tt_kv
# Build app
RUN tt build tt_kv
Разворачиваем кластер с помощью Docker Compose:
services:
tarantool:
build:
context: .
entrypoint: "tt start tt_kv -i"
environment:
APP_USER_PASSWORD: "app"
CLIENT_USER_PASSWORD: "client"
REPLICATOR_USER_PASSWORD: "replicator"
STORAGE_USER_PASSWORD: "storage"
3.1. Проверка и работа с хранилищем
После развертывания кластера вы можете проверить его состояние и выполнить операции с данными, используя утилиту tt
и команды в контейнере Docker.
-
Разворачивание кластера:
Очистите старые контейнеры и запустите новый кластер с пересборкой образа:docker compose rm -f docker compose up --build -d
-
Проверка состояния кластера vshard:
Убедитесь, что маршрутизатор и шарды работают корректно:docker exec tt_kv-tarantool-1 /bin/sh -c "echo \"vshard.router.info()\" | tt connect -x yaml \"tt_kv:router-001-a\""
Эта команда выводит информацию о состоянии маршрутизатора и распределении бакетов.
-
Вставка данных без TTL:
Добавьте паруtest0 = test1
в пространствоkey_value
, которая не будет удаляться по истечению времени:docker exec tt_kv-tarantool-1 /bin/sh -c "echo \"crud.insert_object('key_value', {key = 'test0', value = 'test1', expire_at = 0})\" | tt connect -x yaml \"tt_kv:router-001-a\""
-
Вставка данных с TTL:
Добавьте паруtest2 = test3
в пространствоkey_value
, которая будет удалена через 5 секунд после вставки:docker exec tt_kv-tarantool-1 /bin/sh -c "echo \"crud.insert_object('key_value', {key = 'test2', value = 'test3', expire_at = require('os').time() + 5})\" | tt connect -x yaml \"tt_kv:router-001-a\""
Заключение
Мы создали простое key-value хранилище на Tarantool 3.x с поддержкой шардинга, TTL и метрик Prometheus. Приложение упаковано в Docker-образ и развернуто через Docker Compose. Добавленные команды позволяют легко развернуть кластер и протестировать его функциональность. Этот пример можно расширить, добавив HTTP-API или дополнительные функции, такие как сжатие данных или интеграция с внешними системами.
olku
Есть ли аналог Redis Time Series?