На Хабре уже опубликовано множество статей о платформе 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 = {} -- Получаем курсор с итератором GE по первичному индексу 'id' local index = box.space.key_value.index.id for _, tuple in index:pairs({ prefix }, { iterator = 'GE' }) 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 через vshard, к результатам поиска добавляем метаданные пространства для простоты маппинга:
local function get_by_prefix(prefix) -- init results local results = {} -- find all storages local storages = require('vshard').router.routeall() -- on each storage for _, storage in pairs(storages) do -- call local function local result, err = storage:callro('key_value.get_by_prefix_locally', { prefix }) -- check for error if err then error("Failed to call function on storage: " .. tostring(err)) end -- add to results for _, tuple in ipairs(result) do table.insert(results, {tuple.key, tuple.bucket_id, tuple.value, tuple.expire_at}) end end -- get schema local schema, err = crud.schema('key_value') -- check for error if err then error("Failed to call function on storage: " .. tostring(err)) end -- return return { rows = results, metadata = schema.format } 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.select", "crud.insert" ] 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 или дополнительные функции, такие как сжатие данных или интеграция с внешними системами.
