Yet Another Key-Value Storage на основе Tarantool 3.x

от автора

На Хабре уже опубликовано множество статей о платформе 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) можно подключать сторонние модули, например:

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:

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.

  1. Разворачивание кластера:
    Очистите старые контейнеры и запустите новый кластер с пересборкой образа:

    docker compose rm -f docker compose up --build -d 
  2. Проверка состояния кластера vshard:
    Убедитесь, что маршрутизатор и шарды работают корректно:

    docker exec tt_kv-tarantool-1 /bin/sh -c "echo \"vshard.router.info()\" | tt connect -x yaml \"tt_kv:router-001-a\"" 

    Эта команда выводит информацию о состоянии маршрутизатора и распределении бакетов.

  3. Вставка данных без 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\"" 
  4. Вставка данных с 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 или дополнительные функции, такие как сжатие данных или интеграция с внешними системами.


ссылка на оригинал статьи https://habr.com/ru/articles/920614/