На Хабре уже опубликовано множество статей о платформе 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 или дополнительные функции, такие как сжатие данных или интеграция с внешними системами.

Комментарии (1)


  1. olku
    21.06.2025 20:05

    Есть ли аналог Redis Time Series?