- PVSM.RU - https://www.pvsm.ru -

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

На Хабре уже опубликовано множество статей о платформе Tarantool. Например, есть обзорные материалы [1] о создании key-value хранилищ, но они редко углубляются в детали реализации. Также доступны практические примеры, такие как реализация key-value хранилища [2] на Tarantool 2.x с использованием фреймворка Cartridge [3] и Docker Compose. Однако эти примеры не раскрывают внутренней логики работы приложения.

Цель этой статьи — продемонстрировать процесс создания простого key-value хранилища на основе актуальной версии Tarantool 3.x, а также показать, как его собрать и развернуть.

Исходный код проекта доступен в репозитории [4].

Оглавление

Функциональные требования

  • Запись и обновление пар «ключ-значение».

  • Установка срока жизни (TTL) для записей.

  • Поиск значения по точному совпадению ключа.

  • Поиск записей по префиксу ключа.

Нефункциональные требования

  • Реализация на Tarantool 3.x.

  • Поддержка шардинга.

  • Предоставление метрик в формате Prometheus text-based exposition [28].

  • Поставка в виде Docker-образа и развертывание с помощью Docker Compose.

1. Общая информация о Tarantool

Tarantool — это платформа для вычислений, объединяющая встроенную базу данных и сервер приложений на языке Lua. Она включает модули [29], такие как:

  • box [30] — работа с данными.

  • fiber [31] — управление легковесными потоками для асинхронных задач.

  • http [32] — HTTP-клиент и сервер.

С помощью пакетного менеджера LuaRocks [33] (форк от команды Tarantool: github.com/tarantool/luarocks [34]) можно подключать сторонние модули, например:

1.1. Организация хранения данных

Модуль box [30] отвечает за работу с базой данных. Данные хранятся в spaces [41] — аналогах таблиц в SQL. Spaces содержат tuples [42] — записи базы данных.

Атрибуты space:

  • Уникальное имя, задаваемое пользователем.

  • Уникальный числовой идентификатор (автоматический или пользовательский).

  • Движок (engine): memtx (in-memory) или vinyl (on-disk для больших объемов данных).

Для space можно задавать первичный и вторичные индексы [43]. Схема данных в Tarantool 2.x и 3.x определяется программно через Lua-скрипты. Для миграций схемы в кластере Cartridge (Tarantool 2.x) используется модуль migrations [44].

Пример создания 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 [35], поддерживающий Tarantool 2.x и 3.x. Он применяется и в БД picodata [45]. Tuples делятся на виртуальные сегменты (buckets) [46], которые распределяются между шардами или наборами реплик (replicasets).

Для шардинга нужен индекс (shard_index), по умолчанию — bucket_id. Его имя можно изменить в настройках vshard [47].

Пример создания 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 [48]: хранение buckets. В replicaset один экземпляр — мастер (чтение и запись), остальные — реплики (только чтение).

  • router [49]: маршрутизация запросов. Есть экспериментальный Go VShard Router [50].

  • rebalancer [51]: равномерное распределение buckets (может назначаться автоматически).

1.3. Средства разработки

Для создания приложений и управления экземплярами в Tarantool 2.x применялась утилита Cartridge CLI [52]. В Tarantool 3.x используется новая CLI — tt [53].

Примеры конфигураций на основе tt:

2. Реализация key-value хранилища

2.1. Настройка окружения и создание каркаса проекта

Установим Tarantool и tt. Официальные пакеты доступны для nix-систем, а для Windows — через WSL. Инструкции для Ubuntu [58]:

curl -L https://tarantool.io/repository/3/installer.sh | bash
sudo apt-get install -y tt tarantool

Создадим каркаc проекта на основе шаблона vshard_cluster [59]:

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

Автор: m_karimov

Источник [60]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/key-value-storage/423233

Ссылки в тексте:

[1] обзорные материалы: https://habr.com/ru/companies/vk/articles/829994/

[2] реализация key-value хранилища: https://github.com/tarantool/tarantool-operator/tree/cartridge-0.0.11/examples/kv

[3] Cartridge: https://github.com/tarantool/cartridge

[4] репозитории: https://github.com/MaratKarimov/tt_kv

[5] Yet Another Key-Value Storage на основе Tarantool 3.x: #yet-another-key-value-storage-%D0%BD%D0%B0-%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D0%B5-tarantool-3x

[6] Оглавление: #%D0%BE%D0%B3%D0%BB%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5

[7] Функциональные требования: #%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D1%82%D1%80%D0%B5%D0%B1%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F

[8] Нефункциональные требования: #%D0%BD%D0%B5%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D1%82%D1%80%D0%B5%D0%B1%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F

[9] 1. Общая информация о Tarantool: #1-%D0%BE%D0%B1%D1%89%D0%B0%D1%8F-%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D1%8F-%D0%BE-tarantool

[10] 1.1. Организация хранения данных: #11-%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85

[11] 1.2. Шардирование данных. Кластеры vshard: #12-%D1%88%D0%B0%D1%80%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85-%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D0%B5%D1%80%D1%8B-vshard

[12] 1.3. Средства разработки: #13-%D1%81%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B2%D0%B0-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B8

[13] 2. Реализация key-value хранилища: #2-%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-key-value-%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D0%BB%D0%B8%D1%89%D0%B0

[14] 2.1. Настройка окружения и создание каркаса проекта: #21-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-%D0%BE%D0%BA%D1%80%D1%83%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B8-%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BA%D0%B0%D1%80%D0%BA%D0%B0%D1%81%D0%B0-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0

[15] 2.2. Обновление зависимостей: #22-%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B5%D0%B9

[16] 2.3. Настройка экземпляров storage: #23-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-%D1%8D%D0%BA%D0%B7%D0%B5%D0%BC%D0%BF%D0%BB%D1%8F%D1%80%D0%BE%D0%B2-storage

[17] 2.3.1. Определение схемы данных: #231-%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D1%81%D1%85%D0%B5%D0%BC%D1%8B-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85

[18] 2.3.2. Удаление записей с истекшим TTL: #232-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B5%D0%B9-%D1%81-%D0%B8%D1%81%D1%82%D0%B5%D0%BA%D1%88%D0%B8%D0%BC-ttl

[19] 2.3.3. Поиск по префиксу ключа: #233-%D0%BF%D0%BE%D0%B8%D1%81%D0%BA-%D0%BF%D0%BE-%D0%BF%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D1%83-%D0%BA%D0%BB%D1%8E%D1%87%D0%B0

[20] 2.4. Настройка router: #24-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-router

[21] 2.5. Настройка кластера: #25-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D0%B5%D1%80%D0%B0

[22] 2.5.1. Учетные записи: #251-%D1%83%D1%87%D0%B5%D1%82%D0%BD%D1%8B%D0%B5-%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8

[23] 2.5.2. Роль storage: #252-%D1%80%D0%BE%D0%BB%D1%8C-storage

[24] 2.5.3. Роль router: #253-%D1%80%D0%BE%D0%BB%D1%8C-router

[25] 3. Развертывание хранилища: #3-%D1%80%D0%B0%D0%B7%D0%B2%D0%B5%D1%80%D1%82%D1%8B%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D0%BB%D0%B8%D1%89%D0%B0

[26] 3.1. Проверка и работа с хранилищем: #31-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0-%D0%B8-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0-%D1%81-%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D0%BB%D0%B8%D1%89%D0%B5%D0%BC

[27] Заключение: #%D0%B7%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5

[28] Prometheus text-based exposition: https://github.com/prometheus/docs/blob/main/docs/instrumenting/exposition_formats.md

[29] модули: https://www.tarantool.io/ru/doc/latest/reference/reference_lua/

[30] box: https://www.tarantool.io/ru/doc/latest/reference/reference_lua/box/

[31] fiber: https://www.tarantool.io/ru/doc/latest/reference/reference_lua/fiber/

[32] http: https://github.com/tarantool/http

[33] LuaRocks: https://luarocks.org/

[34] github.com/tarantool/luarocks: https://github.com/tarantool/luarocks

[35] vshard: https://github.com/tarantool/vshard

[36] crud: https://github.com/tarantool/crud

[37] expirationd: https://github.com/tarantool/expirationd

[38] github.com/tarantool/mysql: https://github.com/tarantool/mysql

[39] github.com/tarantool/pg: https://github.com/tarantool/pg

[40] metrics-export-role: https://github.com/tarantool/metrics-export-role

[41] spaces: https://www.tarantool.io/ru/doc/latest/platform/ddl_dml/value_store/#index-box-space

[42] tuples: https://www.tarantool.io/ru/doc/latest/platform/ddl_dml/value_store/#term-tuple

[43] индексы: https://www.tarantool.io/ru/doc/latest/platform/ddl_dml/indexes/#index-box-index

[44] migrations: https://github.com/tarantool/migrations

[45] picodata: https://github.com/picodata/vshard

[46] виртуальные сегменты (buckets): https://www.tarantool.io/ru/doc/latest/platform/sharding/vshard_architecture/#vshard-vbuckets

[47] настройках vshard: https://www.tarantool.io/en/doc/latest/reference/reference_rock/vshard/vshard_ref/#cfg-basic-shard-index

[48] storage: https://www.tarantool.io/ru/doc/latest/platform/sharding/vshard_architecture/#vshard-architecture-storage

[49] router: https://www.tarantool.io/ru/doc/latest/platform/sharding/vshard_architecture/#vshard

[50] Go VShard Router: https://github.com/tarantool/go-vshard-router/blob/master/README_ru.md

[51] rebalancer: https://www.tarantool.io/ru/doc/latest/platform/sharding/vshard_architecture/#vshard-rebalancer

[52] Cartridge CLI: https://github.com/tarantool/cartridge-cli

[53] tt: https://github.com/tarantool/tt

[54] create_db: https://github.com/tarantool/doc/tree/main/doc/code_snippets/snippets/config/instances.enabled/create_db

[55] sharded_cluster: https://github.com/tarantool/doc/tree/main/doc/code_snippets/snippets/sharding/instances.enabled/sharded_cluster

[56] sharded_cluster_crud: https://github.com/tarantool/doc/tree/main/doc/code_snippets/snippets/sharding/instances.enabled/sharded_cluster_crud

[57] sharded_cluster_crud_metrics: https://github.com/tarantool/doc/tree/main/doc/code_snippets/snippets/sharding/instances.enabled/sharded_cluster_crud_metrics

[58] Ubuntu: https://www.tarantool.io/ru/download/os-installation/ubuntu/

[59] vshard_cluster: https://github.com/tarantool/tt/blob/master/cli/create/builtin_templates/templates/vshard_cluster

[60] Источник: https://habr.com/ru/articles/920614/?utm_source=habrahabr&utm_medium=rss&utm_campaign=920614