Рейтинг@Mail.ru
Документация Tarantool 1.10.4
Документация Tarantool 1.10.4

Документация Tarantool 1.10.4

Замечание

Документация находится в процессе перевода и может отставать от английской версии.

Tarantool - Documentation

Руководство для начинающих

В этой главе объясняются основы работы с Tarantool как с СУБД, а также приводятся способы подключения к базе на Tarantool из других языков программирования.

Создаем свою первую базу данных на Tarantool

Первым делом давайте установим Tarantool, запустим его и создадим простую базу данных.

Вы можете установить Tarantool и работать с ним либо локально, либо в Docker – как вам удобнее.

Использование Docker-образа

Для практики и тестирования мы рекомендуем использовать официальные образы Tarantool’а для Docker. Официальный образ содержит определенную версию Tarantool’а и все популярные внешние модули для Tarantool’а. Все необходимое уже установлено и настроено на платформе Linux. Данные образы - это самый простой способ установить и запустить Tarantool.

Примечание

Если вы никогда раньше не работали с Docker, рекомендуем сперва прочитать эту обучающую статью.

Запуск контейнера

Если Docker не установлен на вашей машине, следуйте официальным инструкциям по установке для вашей ОС.

Для использования полнофункционального экземпляра Tarantool’а запустите контейнер с минимальными настройками:

$ docker run \
  --name mytarantool \
  -d -p 3301:3301 \
  -v /data/dir/on/host:/var/lib/tarantool \
  tarantool/tarantool:1

Эта команда запускает новый контейнер с именем „mytarantool“. Docker запускает его из официального образа „tarantool/tarantool:1“ с предустановленным Tarantool’ом 1.10 и всеми внешними модулями.

Tarantool будет принимать входящие подключения по адресу localhost:3301. Можно сразу начать его использовать как key-value хранилище.

Tarantool сохраняет данные внутри контейнера. Чтобы ваше тестовые данные остались доступны после остановки контейнера, эта команда также монтирует директорию /data/dir/on/host (здесь необходимо указать абсолютный путь до существующей локальной директории), расположенную на машине, в директорию /var/lib/tarantool (Tarantool традиционно использует эту директорию в контейнере для сохранения данных), расположенную в контейнере. Таким образом все изменения в смонтированной директории, внесенные на стороне контейнера, также отражаются в расположенной на пользовательском диске директории.

Модуль Tarantool’а для работы с базой данных уже настроен и запущен в контейнере. Ручная настройка не требуется, если только вы не используете Tarantool как сервер приложений и не запускаете его вместе с приложением.

Примечание

Если ваш контейнер рушится вскоре после запуска, перейдите на эту страницу, чтобы найти возможное решение.

Подключение к экземпляру Tarantool’а

Для подключения к запущенному в контейнере экземпляру Tarantool’а, выполните эту команду:

$ docker exec -i -t mytarantool console

Эта команда:

  • Требует от Tarantool’а открыть порт с интерактивной консолью для входящих подключений.
  • Подключается через стандартный Unix-сокет к Tarantool-серверу, запущенному внутри контейнера, из-под пользователя admin.

Tarantool показывает приглашение командной строки:

tarantool.sock>

Теперь вы можете вводить запросы в командной строке.

Примечание

На боевых серверах интерактивный режим Tarantool’а предназначен только для системных администраторов. Мы же используем его в большинстве примеров в данном руководстве, потому что интерактивный режим хорошо подходит для обучения.

Создание базы данных

Подключившись к консоли, давайте создадим простую тестовую базу данных.

Сначала создайте первый спейс (с именем tester):

tarantool.sock> s = box.schema.space.create('tester')

Форматируйте созданный спейс, указав имена и типы полей:

tarantool.sock> s:format({
              > {name = 'id', type = 'unsigned'},
              > {name = 'band_name', type = 'string'},
              > {name = 'year', type = 'unsigned'}
              > })

Создайте первый индекс (с именем primary):

tarantool.sock> s:create_index('primary', {
              > type = 'hash',
              > parts = {'id'}
              > })

Это первичный индекс по полю id в каждом кортеже.

Вставьте в созданный спейс три кортежа (наш термин для записей):

tarantool.sock> s:insert{1, 'Roxette', 1986}
tarantool.sock> s:insert{2, 'Scorpions', 2015}
tarantool.sock> s:insert{3, 'Ace of Base', 1993}

Для выборки кортежей по первичному индексу primary выполните команду:

tarantool.sock> s:select{3}

Теперь вывод в окне терминала выглядит следующим образом:

tarantool.sock> s = box.schema.space.create('tester')
---
...
tarantool.sock> s:format({
              > {name = 'id', type = 'unsigned'},
              > {name = 'band_name', type = 'string'},
              > {name = 'year', type = 'unsigned'}
              > })
---
...
tarantool.sock> s:create_index('primary', {
              > type = 'hash',
              > parts = {'id'}
              > })
---
- unique: true
  parts:
  - type: unsigned
    is_nullable: false
    fieldno: 1
  id: 0
  space_id: 512
  name: primary
  type: HASH
...
tarantool.sock> s:insert{1, 'Roxette', 1986}
---
- [1, 'Roxette', 1986]
...
tarantool.sock> s:insert{2, 'Scorpions', 2015}
---
- [2, 'Scorpions', 2015]
...
tarantool.sock> s:insert{3, 'Ace of Base', 1993}
---
- [3, 'Ace of Base', 1993]
...
tarantool.sock> s:select{3}
---
- - [3, 'Ace of Base', 1993]
...

Для добавления вторичного индекса по полю band_name используйте эту команду:

tarantool.sock> s:create_index('secondary', {
              > type = 'hash',
              > parts = {'band_name'}
              > })

Для выборки кортежей по вторичному индексу secondary выполните команду:

tarantool.sock> s.index.secondary:select{'Scorpions'}
---
- - [2, 'Scorpions', 2015]
...

Чтобы удалить индекс, выполните:

tarantool> s.index.secondary:drop()
---
...

Остановка контейнера

После завершения тестирования для корректной остановки контейнера выполните эту команду:

$ docker stop mytarantool

Это был временный контейнер, поэтому после остановки содержимое его диска/памяти обнулилось. Но так как вы монтировали локальную директорию в контейнер, все данные Tarantool’а сохранились на диске вашей машины. Если вы запустите новый контейнер и смонтируете в него ту же директорию с данными, Tarantool восстановит все данные с диска и продолжит с ними работать.

Использование менеджера пакетов

Для промышленной разработки мы рекомендуем устанавливать Tarantool с помощью официального менеджера пакетов. Можно выбрать одну из трех версий: LTS, stable или beta. Автоматическая система сборки создает, тестирует и публикует пакеты после каждого коммита в соответствующую ветку репозитория Tarantool’а на GitHub.

Чтобы скачать и установить подходящий пакет, откройте командную строку и введите инструкции, которые даны для вашей операционной системы на странице для скачивания.

Запуск экземпляра Tarantool’а

Чтобы начать работу с Tarantool, выполните эту команду:

$ tarantool
$ # при этом создается новый экземпляр Tarantool

Tarantool запускается в интерактивном режиме и показывает приглашение командной строки:

tarantool>

Теперь вы можете вводить запросы в командной строке.

Примечание

На боевых серверах интерактивный режим Tarantool’а предназначен только для системных администраторов. Мы же используем его в большинстве примеров в данном руководстве, потому что интерактивный режим хорошо подходит для обучения.

Создание базы данных

Далее объясняется, как создать простую тестовую базу данных после установки Tarantool’а.

  1. Чтобы Tarantool хранил данные в определенном месте, создайте предназначенную специально для тестов директорию:

    $ mkdir ~/tarantool_sandbox
    $ cd ~/tarantool_sandbox
    

    Ее можно удалить после окончания тестирования.

  2. Проверьте доступность порта, используемого по умолчанию для прослушивания на экземпляре базы данных.

    В зависимости от версии, Tarantool может во время установки запустить экземпляр example.lua, который настроен на прослушивание по порту 3301 по умолчанию. В файле example.lua показана базовая конфигурация; его можно найти в директории /etc/tarantool/instances.enabled или /etc/tarantool/instances.available.

    Тем не менее, мы предлагаем провести установку самостоятельно с целью обучения.

    Убедитесь, что свободен порт, используемый по умолчанию:

    1. Чтобы проверить статус работы демонстрационного экземпляра, выполните команду:

      $ lsof -i :3301
      COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
      tarantool 6851 root   12u  IPv4  40827      0t0  TCP *:3301 (LISTEN)
      
    2. Если он запущен, отключите соответствующий процесс. В данном примере:

      $ kill 6851
      
  3. Чтобы запустить модуль Tarantool’а для работы с базой данных и сделать так, чтобы запущенный экземпляр принимал TCP-запросы на порту 3301, выполните эту команду:

    tarantool> box.cfg{listen = 3301}
    
  4. Создайте первый спейс (с именем tester):

    tarantool> s = box.schema.space.create('tester')
    
  5. Форматируйте созданный спейс, указав имена и типы полей:

    tarantool> s:format({
             > {name = 'id', type = 'unsigned'},
             > {name = 'band_name', type = 'string'},
             > {name = 'year', type = 'unsigned'}
             > })
    
  6. Создайте первый индекс (с именем primary):

    tarantool> s:create_index('primary', {
             > type = 'hash',
             > parts = {'id'}
             > })
    

    Это первичный индекс по полю id в каждом кортеже.

  7. Вставьте в созданный спейс три кортежа (наш термин для записей):

    tarantool> s:insert{1, 'Roxette', 1986}
    tarantool> s:insert{2, 'Scorpions', 2015}
    tarantool> s:insert{3, 'Ace of Base', 1993}
    
  8. Для выборки кортежей по первичному индексу primary выполните команду:

    tarantool> s:select{3}
    

    Теперь вывод в окне терминала выглядит следующим образом:

    tarantool> s = box.schema.space.create('tester')
    ---
    ...
    tarantool> s:format({
             > {name = 'id', type = 'unsigned'},
             > {name = 'band_name', type = 'string'},
             > {name = 'year', type = 'unsigned'}
             > })
    ---
    ...
    tarantool> s:create_index('primary', {
             > type = 'hash',
             > parts = {'id'}
             > })
    ---
    - unique: true
      parts:
      - type: unsigned
        is_nullable: false
        fieldno: 1
      id: 0
      space_id: 512
      name: primary
      type: HASH
    ...
    tarantool> s:insert{1, 'Roxette', 1986}
    ---
    - [1, 'Roxette', 1986]
    ...
    tarantool> s:insert{2, 'Scorpions', 2015}
    ---
    - [2, 'Scorpions', 2015]
    ...
    tarantool> s:insert{3, 'Ace of Base', 1993}
    ---
    - [3, 'Ace of Base', 1993]
    ...
    tarantool> s:select{3}
    ---
    - - [3, 'Ace of Base', 1993]
    ...
    
  9. Для добавления вторичного индекса по полю band_name используйте эту команду:

    tarantool> s:create_index('secondary', {
             > type = 'hash',
             > parts = {'band_name'}
             > })
    
  10. Для выборки кортежей по вторичному индексу secondary выполните команду:

    tarantool> s.index.secondary:select{'Scorpions'}
    ---
    - - [2, 'Scorpions', 2015]
    ...
    
  11. Теперь, чтобы подготовиться к примеру в следующем разделе, попробуйте следующее:

    tarantool> box.schema.user.grant('guest', 'read,write,execute', 'universe')
    

Установка удаленного подключения

В запросе box.cfg{listen = 3301}, который мы отправили ранее, параметр listen может принимать в качестве значения URI (унифицированный идентификатор ресурса) любой формы. В нашем случае это просто локальный порт 3301. Вы можете отправлять запросы на указанный URI, используя:

  1. telnet,
  2. коннектор,
  3. другой экземпляр Tarantool’а (с помощью модуля console), либо
  4. утилиту tarantoolctl.

Давайте попробуем вариант с tarantoolctl.

Переключитесь на другой терминал. Например, в Linux-системе для этого нужно запустить еще один экземпляр Bash. В новом терминале можно сменить текущую рабочую директорию на любую другую, необязательно использовать ~/tarantool_sandbox.

Запустите утилиту tarantoolctl:

$ tarantoolctl connect '3301'

Данная команда означает «использовать утилиту tarantoolctl для подключения к Tarantool-серверу, который слушает по адресу localhost:3301».

Введите следующий запрос:

localhost:3301> box.space.tester:select{2}

Это означает «послать запрос тому Tarantool-серверу и вывести результат на экран». Результатом в данном случае будет один из кортежей, что вы вставляли ранее. В окне терминала теперь должно отображаться примерно следующее:

$ tarantoolctl connect 3301
/usr/local/bin/tarantoolctl: connected to localhost:3301
localhost:3301> box.space.tester:select{2}
---
- - [2, 'Scorpions', 2015]
...

Вы можете посылать запросы box.space...:insert{} и box.space...:select{} неограниченное количество раз на любом из двух запущенных экземпляров Tarantool’а.

Закончив тестирование, выполните следующие шаги:

  • Для удаления спейса: s:drop()
  • Для остановки tarantoolctl: ctrl+C или ctrl+D
  • Для остановки Tarantool’а (альтернативный вариант): стандартная Lua-функция os.exit()
  • Для остановки Tarantool’а (из другого терминала): sudo pkill -f tarantool
  • Для удаления директории-песочницы: rm -r ~/tarantool_sandbox

Подключаемся к базе из разных языков программирования

Итак, мы создали базу данных в Tarantool. Теперь давайте посмотрим, как к ней можно подключиться из Python, PHP и Go.

Подключение из Python

Подготовка

Перед тем как идти дальше, выполним следующие действия:

  1. Установим библиотеку tarantool. Рекомендуется использовать python3 и pip3.

  2. Запустим Tarantool (локально или в Docker) и обязательно создадим базу данных с тестовыми данными, как показано в предыдущем разделе:

    box.cfg{listen = 3301}
    s = box.schema.space.create('tester')
    s:format({
             {name = 'id', type = 'unsigned'},
             {name = 'band_name', type = 'string'},
             {name = 'year', type = 'unsigned'}
             })
    s:create_index('primary', {
             type = 'hash',
             parts = {'id'}
             })
    s:create_index('secondary', {
             type = 'hash',
             parts = {'band_name'}
             })
    s:insert{1, 'Roxette', 1986}
    s:insert{2, 'Scorpions', 2015}
    s:insert{3, 'Ace of Base', 1993}
    

    Важно

    Не закрывайте окно терминала с запущенным Tarantool – оно пригодится нам позднее.

  3. Чтобы иметь возможность подключаться к Tarantool в качестве администратора, сменим пароль пользователя admin:

    box.schema.user.passwd('pass')
    

Подключение к Tarantool

Для подключения к серверу достаточно выполнить следующее:

>>> import tarantool
>>> connection = tarantool.connect("localhost", 3301)

Также при необходимости можно указать имя пользователя и пароль:

>>> tarantool.connect("localhost", 3301, user=username, password=password)

По умолчанию используется пользователь guest.

Работа с данными

Спейс – это контейнер для кортежей. Чтобы обратиться к спейсу как к именованному объекту, воспользуемся функцией connection.space:

>>> tester = connection.space('tester')
Вставка данных

Для вставки нового кортежа в спейс воспользуемся функцией insert:

>>> tester.insert((4, 'ABBA', 1972))
[4, 'ABBA', 1972]
Получение данных

Сначала выберем кортеж по первичному ключу (в нашем примере первичный индекс ––это индекс primary, построенный по полю id в каждом кортеже). Воспользуемся функцией select:

>>> tester.select(4)
[4, 'ABBA', 1972]

Теперь поищем кортежи по вторичному ключу. Для этого нужно указать номер или имя вторичного индекса.

Сначала сделаем запрос по номеру индекса:

>>> tester.select('Scorpions', index=1)
[2, 'Scorpions', 2015]

(Мы указываем index=1, потому что индексы в Tarantool нумеруются с нуля, а в данном случае мы обращаемся к индексу, который создавали вторым.)

Теперь сделаем аналогичный запрос по имени индекса и получим тот же результат:

>>> tester.select('Scorpions', index='secondary')
[2, 'Scorpions', 2015]

А чтобы выбрать все кортежи из спейса, вызовем select без аргументов:

>>> tester.select()
Обновление данных

Обновим значение поля с помощью update:

>>> tester.update(4, [('=', 1, 'New group'), ('+', 2, 2)])

Здесь мы обновляем значение поля 1 и увеличиваем значение поля 2 для кортежа с id = 4. Если кортежа с таким id нет, то Tarantool вернет ошибку.

Теперь с помощью функции replace мы полностью заменим кортеж с совпадающим первичным ключом. Если кортежа с указанным первичным ключом не существует, то эта операция ни к чему не приведет.

>>> tester.replace((4, 'New band', 2015))

Также мы можем обновлять данные с помощью функции upsert, которая работает аналогично update, но создает новый кортеж, если старый не был найден.

>>> tester.upsert((4, 'Another band', 2000), [('+', 2, 5)])

Здесь мы увеличиваем на 5 значение поля 2 в кортеже с id = 4 – или же вставляем кортеж (4, "Another band", 2000), если такого нет.

Удаление данных

Чтобы удалить кортеж, нужно использовать delete(primary_key):

>>> tester.delete(4)
[4, 'New group', 2012]

Для удаления всех кортежей в спейсе (или всего спейса целиком) нужно воспользоваться функцией call. Мы поговорим о ней подробнее в следующем разделе.

Чтобы удалить все кортежи в спейсе, нужно вызвать функцию space:truncate:

>>> connection.call('box.space.tester:truncate', ())

Чтобы удалить весь спейс, нужно вызвать функцию space:drop. Для выполнения следующей команды необходимо подключиться из-под пользователя admin:

>>> connection.call('box.space.tester:drop', ())

Исполнение хранимых процедур

Перейдем в терминал с запущенным Tarantool.

Примечание

О том, как установить удаленное подключение к Tarantool, можно прочитать здесь:

Напишем простую функцию на Lua:

function sum(a, b)
    return a + b
end

Итак, теперь у нас есть функция, описанная в Tarantool. Чтобы вызвать ее из python, нам нужна функция call:

>>> connection.call('sum', (3, 2))
5

Также мы можем передать на выполнение любой Lua-код. Для этого воспользуемся функцией eval:

>>> connection.eval('return 4 + 5')
9

Подключение из PHP

Подготовка

Перед тем как идти дальше, выполним следующие действия:

  1. Установим библиотеку tarantool/client.

  2. Запустим Tarantool (локально или в Docker) и обязательно создадим базу данных с тестовыми данными, как показано в предыдущем разделе:

    box.cfg{listen = 3301}
    s = box.schema.space.create('tester')
    s:format({
             {name = 'id', type = 'unsigned'},
             {name = 'band_name', type = 'string'},
             {name = 'year', type = 'unsigned'}
             })
    s:create_index('primary', {
             type = 'hash',
             parts = {'id'}
             })
    s:create_index('secondary', {
             type = 'hash',
             parts = {'band_name'}
             })
    s:insert{1, 'Roxette', 1986}
    s:insert{2, 'Scorpions', 2015}
    s:insert{3, 'Ace of Base', 1993}
    

    Важно

    Не закрывайте окно терминала с запущенным Tarantool – оно пригодится нам позднее.

  3. Чтобы иметь возможность подключаться к Tarantool в качестве администратора, сменим пароль пользователя admin:

    box.schema.user.passwd('pass')
    

Подключение к Tarantool

Для настройки подключения к серверу достаточно выполнить следующее:

use Tarantool\Client\Client;

require __DIR__.'/vendor/autoload.php';
$client = Client::fromDefaults();

Само подключение будет установлено при первом запросе. Также при необходимости можно указать имя пользователя и пароль:

$client = Client::fromOptions([
    'uri' => 'tcp://127.0.0.1:3301',
    'username' => '<username>',
    'password' => '<password>'
]);

По умолчанию используется пользователь guest.

Работа с данными

Спейс – это контейнер для кортежей. Чтобы обратиться к спейсу как к именованному объекту, воспользуемся функцией getSpace:

$tester = $client->getSpace('tester');
Вставка данных

Для вставки нового кортежа в спейс воспользуемся функцией insert:

$result = $tester->insert([4, 'ABBA', 1972]);
Получение данных

Сначала выберем кортеж по первичному ключу (в нашем примере первичный индекс ––это индекс primary, построенный по полю id в каждом кортеже). Воспользуемся функцией select:

use Tarantool\Client\Schema\Criteria;

$result = $tester->select(Criteria::key([4]));
printf(json_encode($result));
[[4, 'ABBA', 1972]]

Теперь поищем кортежи по вторичному ключу. Для этого нужно указать номер или имя вторичного индекса.

Сначала сделаем запрос по номеру индекса:

$result = $tester->select(Criteria::index(1)->andKey(['Scorpions']));
printf(json_encode($result));
[2, 'Scorpions', 2015]

(Мы указываем index(1), потому что индексы в Tarantool нумеруются с нуля, а в данном случае мы обращаемся к индексу, который создавали вторым.)

Теперь сделаем аналогичный запрос по имени индекса и получим тот же результат:

$result = $tester->select(Criteria::index('secondary')->andKey(['Scorpions']));
printf(json_encode($result));
[2, 'Scorpions', 2015]

А чтобы выбрать все кортежи из спейса, вызовем select:

$result = $tester->select(Criteria::allIterator());
Обновление данных

Обновим значение поля с помощью update:

use Tarantool\Client\Schema\Operations;

$result = $tester->update([4], Operations::set(1, 'New group')->andAdd(2, 2));

Здесь мы обновляем значение поля 1 и увеличиваем значение поля 2 для кортежа с id = 4. Если кортежа с таким id нет, то Tarantool вернет ошибку.

Теперь с помощью функции replace мы полностью заменим кортеж с совпадающим первичным ключом. Если кортежа с указанным первичным ключом не существует, то эта операция ни к чему не приведет.

$result = $tester->replace([4, 'New band', 2015]);

Также мы можем обновлять данные с помощью функции upsert, которая работает аналогично update, но создает новый кортеж, если старый не был найден.

use Tarantool\Client\Schema\Operations;

$tester->upsert([4, 'Another band', 2000], Operations::add(2, 5));

Здесь мы увеличиваем на 5 значение поля 2 в кортеже с id = 4 – или же вставляем кортеж (4, "Another band", 2000), если такого нет.

Удаление данных

Чтобы удалить кортеж, нужно использовать delete(primary_key):

$result = $tester->delete([4]);

Для удаления всех кортежей в спейсе (или всего спейса целиком) нужно воспользоваться функцией call. Мы поговорим о ней подробнее в следующем разделе.

Чтобы удалить все кортежи в спейсе, нужно вызвать функцию space:truncate:

$result = $client->call('box.space.tester:truncate');

Чтобы удалить весь спейс, нужно вызвать функцию space:drop. Для выполнения следующей команды необходимо подключиться из-под пользователя admin:

$result = $client->call('box.space.tester:drop');

Исполнение хранимых процедур

Перейдем в терминал с запущенным Tarantool.

Примечание

О том, как установить удаленное подключение к Tarantool, можно прочитать здесь:

Напишем простую функцию на Lua:

function sum(a, b)
    return a + b
end

Итак, теперь у нас есть функция, описанная в Tarantool. Чтобы вызвать ее из php, нам нужна функция call:

$result = $client->call('sum', 3, 2);

Также мы можем передать на выполнение любой Lua-код. Для этого воспользуемся функцией eval:

$result = $client->evaluate('return 4 + 5');

Подключение из Go

Подготовка

Перед тем как идти дальше, выполним следующие действия:

  1. Установим библиотеку go-tarantool.

  2. Запустим Tarantool (локально или в Docker) и обязательно создадим базу данных с тестовыми данными, как показано в предыдущем разделе:

    box.cfg{listen = 3301}
    s = box.schema.space.create('tester')
    s:format({
             {name = 'id', type = 'unsigned'},
             {name = 'band_name', type = 'string'},
             {name = 'year', type = 'unsigned'}
             })
    s:create_index('primary', {
             type = 'hash',
             parts = {'id'}
             })
    s:create_index('secondary', {
             type = 'hash',
             parts = {'band_name'}
             })
    s:insert{1, 'Roxette', 1986}
    s:insert{2, 'Scorpions', 2015}
    s:insert{3, 'Ace of Base', 1993}
    

    Важно

    Не закрывайте окно терминала с запущенным Tarantool – оно пригодится нам позднее.

  3. Чтобы иметь возможность подключаться к Tarantool в качестве администратора, сменим пароль пользователя admin:

    box.schema.user.passwd('pass')
    

Подключение к Tarantool

Простая программа, выполняющая подключение к серверу, будет выглядеть так:

package main

import (
    "fmt"

    "github.com/tarantool/go-tarantool"
)

func main() {

    conn, err := tarantool.Connect("127.0.0.1:3301", tarantool.Opts{
            User: "admin",
            Pass: "pass",
    })

    if err != nil {
            log.Fatalf("Connection refused")
    }

    defer conn.Close()

    // Ваш код общения с базой

}

По умолчанию используется пользователь guest.

Работа с данными

Вставка данных

Для вставки нового кортежа в спейс воспользуемся функцией Insert:

resp, err = conn.Insert("tester", []interface{}{4, "ABBA", 1972})

В этом примере в спейс tester вставляется кортеж (4, "ABBA", 1972).

Код ответа и данные можно получить из структуры tarantool.Response:

code := resp.Code
data := resp.Data
Получение данных

Чтобы выбрать кортеж из спейса, воспользуемся функцией Select:

resp, err = conn.Select("tester", "primary", 0, 1, tarantool.IterEq, []interface{}{4})

В этом примере мы ищем кортеж по первичному ключу с offset = 0 и limit = 1 в спейсе tester (первичный индекс в нашем примере – это индекс primary, построенный по полю id в каждом кортеже).

Теперь поищем по вторичному ключу:

resp, err = conn.Select("tester", "secondary", 0, 1, tarantool.IterEq, []interface{}{"ABBA"})

Наконец, было бы интересно сделать полную выборку даных из спейса. Но в рамках языка Go эта задача не решается в одну строчку. Пример такой программы вы можете посмотреть в отдельном разделе документации.

Более сложные примеры выборок можно увидеть тут: https://github.com/tarantool/go-tarantool#usage

Обновление данных

Обновим значение поля с помощью Update:

resp, err = conn.Update("tester", "primary", []interface{}{4}, []interface{}{[]interface{}{"+", 2, 3}})

Здесь мы увеличиваем на 3 значение поля 2 для кортежа с id = 4. Если кортежа с таким id нет, то Tarantool вернет ошибку.

Теперь с помощью функции Replace мы полностью заменим кортеж с совпадающим первичным ключом. Если кортежа с указанным первичным ключом не существует, то эта операция ни к чему не приведет.

resp, err = conn.Replace("tester", []interface{}{4, "New band", 2011})

Также мы можем обновлять данные с помощью функции Upsert, которая работает аналогично Update, но создает новый кортеж, если старый не был найден.

resp, err = conn.Upsert("tester", []interface{}{4, "Another band", 2000}, []interface{}{[]interface{}{"+", 2, 5}})

Здесь мы увеличиваем на 5 значение третьего поля в кортеже с id = 4 – или же вставляем кортеж (4, "Another band", 2000), если такого нет.

Удаление данных

Чтобы удалить кортеж, воспользуемся функцией сonnection.Delete:

resp, err = conn.Delete("tester", "primary", []interface{}{4})

Для удаления всех кортежей в спейсе (или всего спейса целиком), нужно воспользоваться функцией Call. Мы поговорим о ней подробнее в следующем разделе.

Чтобы удалить все кортежи в спейсе, нужно вызвать функцию space:truncate:

resp, err = conn.Call("box.space.tester:truncate", []interface{}{})

Чтобы удалить весь спейс, нужно вызвать функцию space:drop. Для выполнения следующей команды необходимо подключиться из-под пользователя admin:

resp, err = conn.Call("box.space.tester:drop", []interface{}{})

Исполнение хранимых процедур

Перейдем в терминал с запущенным Tarantool.

Примечание

О том, как установить удаленное подключение к Tarantool, можно прочитать здесь:

Напишем простую функцию на Lua:

function sum(a, b)
    return a + b
end

Итак, теперь у нас есть функция, описанная в Tarantool. Чтобы вызвать ее из go, нам нужна функция Call:

resp, err = conn.Call("sum", []interface{}{2, 3})

Также мы можем передать на выполнение любой Lua-код. Для этого воспользуемся функцией Eval:

resp, err = connection.Eval("return 4 + 5", []interface{}{})

Создаем свое первое приложение на Tarantool Cartridge

Здесь мы показываем, как сделать простое кластерное приложение.

Первым делом настройте среду разработки.

Затем создайте приложение с именем myapp. Выполните:

$ cartridge create --name myapp

Эта команда создает новое Tarantool Cartridge-приложение в директории ./myapp. Там теперь содержатся созданные по шаблону файлы и директории.

Войдите внутрь этой директории и запустите ваше приложение:

$ cd ./myapp
$ cartridge build
$ cartridge start

Эта команда собирает приложение локально, стартует 5 экземпляров Tarantool и запускает приложение в том виде, как оно было создано – без какой-либо интересной бизнес-логики.

Откуда взялись 5 экземпляров? Загляните внутрь файла instances.yml.Там задается конфигурация всех экземпляров, которые вы можете настроить внутри вашего кластера. По умолчанию, там задана конфигурация 5 экземпляров.

myapp.router:
  workdir: ./tmp/db_dev/3301
  advertise_uri: localhost:3301
  http_port: 8081

myapp.s1-master:
  workdir: ./tmp/db_dev/3302
  advertise_uri: localhost:3302
  http_port: 8082

myapp.s1-replica:
  workdir: ./tmp/db_dev/3303
  advertise_uri: localhost:3303
  http_port: 8083

myapp.s2-master:
  workdir: ./tmp/db_dev/3304
  advertise_uri: localhost:3304
  http_port: 8084

myapp.s2-replica:
  workdir: ./tmp/db_dev/3305
  advertise_uri: localhost:3305
  http_port: 8085

Вы можете увидеть все эти экземпляры в веб-интерфейсе для управления кластером по адресу http://localhost:8081 (порт 8081 – это HTTP-порт первого экземпляра из файла instances.yml).

_images/cluster_dry_run-border-5px.png

Теперь временно остановите кластер с помощью Ctrl + C.

Пора заняться написанием бизнес-логики для вашего приложения. Чтобы не слишком усложнять наш пример, возьмем канонический «Hello world!»«.

Переименуйте шаблонный файл app/roles/custom.lua в hello-world.lua.

$ mv app/roles/custom.lua app/roles/hello-world.lua

Это будет ваша роль. Роль в Tarantool Cartridge – это Lua-модуль, в котором реализованы специфичные для экземпляра Tarantool функции и логика. Далее мы покажем, как добавлять в роль свой код, собирать ее, назначать и тестировать.

У вашей роли уже есть некоторый код внутри функции init().

 local function init(opts) -- luacheck: no unused args
     -- if opts.is_master then
     -- end

     local httpd = cartridge.service_get('httpd')
     httpd:route({method = 'GET', path = '/hello'}, function()
         return {body = 'Hello world!'}
     end)

     return true
 end

Этот код экспортирует конечную точку /hello для выполнения HTTP-запросов. Например, для первого экземпляра из файла instances.yml это будет http://localhost:8081/hello . Если вы зайдете по этому адресу в браузере после того, как роль будет назначена (чуть позже мы покажем, как это делается), то увидите на странице слова «Hello world!».

Добавим сюда еще немного кода.

 local function init(opts) -- luacheck: no unused args
     -- if opts.is_master then
     -- end

     local httpd = cartridge.service_get('httpd')
     httpd:route({method = 'GET', path = '/hello'}, function()
         return {body = 'Hello world!'}
     end)

     local log = require('log')
     log.info('Hello world!')

     return true
 end

Здесь мы пишем «Hello, world!» в консоль в момент назначения роли, что даст вам возможность отследить данное событие. Пока ничего сложного.

Далее изменим значение параметра role_name в «return»-блоке файла hello-world.lua. Этот текст будет показан в качестве имени вашей роли в веб-интерфейсе для управления кластером

 return {
     role_name = 'Hello world!',
     init = init,
     stop = stop,
     validate_config = validate_config,
     apply_config = apply_config,
 }

Последнее, что осталось сделать – это добавить ваше роль в список доступных ролей кластера, в файл init.lua.

 local ok, err = cartridge.cfg({
     workdir = 'tmp/db',
     roles = {
         'cartridge.roles.vshard-storage',
         'cartridge.roles.vshard-router',
         'app.roles.hello-world'
     },
     cluster_cookie = 'myapp-cluster-cookie',
 })

Теперь кластер будет знать про вашу роль.

Почему мы указали app.roles.hello-world? По умолчанию, имя роли в данном файле должно включать в себя полный путь от корня приложения (./myapp) до файла роли (app/roles/hello-world.lua).

Отлично! Роль готова. Теперь заново соберите и запустите ваше приложение:

$ cartridge build
$ cartridge start

Все экземпляры запущены, но они пока ничего не делают, а ждут, что им назначат роли.

Экземпляры (реплики) в кластере Tarantool Cartridge должны быть собраны в наборы реплик. Роли назначаются каждому набору, и любой экземпляр в наборе реплик видит все роли, которые назначены этому набору.

Давайте создадим набор реплик, в котором будет всего один экземпляр, и назначим этому набору вашу роль.

  1. Откройте веб-интерфейс для управления кластером по адресу http://localhost:8081.

  2. Нажмите кнопку Configure.

  3. Установите флажок напротив роли Hello world!, чтобы назначить ее. Заметьте, что имя роли здесь совпадает с тем текстом, который вы задали в параметре role_name в файле hello-world.lua.

  4. (По желанию) Задайте имя набора реплик, например «hello-world-replica-set».

    _images/cluster_create_replica_set-border-5px.png
  5. Нажмите кнопку Create replica set. Информация о вашем наборе реплик появится в веб-интерфейсе.

_images/cluster_new_replica_set-border-5px.png

Итак, ваша роль назначена. В консоли вы можете увидеть такое сообщение:

_images/cluster_hello_world_console-border-5px.png

А если вы сейчас откроете в браузере страницу http://localhost:8081/hello , то увидите ответ вашей роли на HTTP GET-запрос.

_images/cluster_hello_http-border-5px.png

Все работает! Что же дальше?

  • Загляните в это руководство, чтобы настроить оставшиеся наборы реплик и опробовать разные кластерные возможности.
  • Посмотрите эти примеры приложений и реализуйте более сложную логику для вашей роли.
  • Упакуйте ваше приложение для дальнейшего распространения. Вы можете выбрать любой из поддерживаемых видов пакетов: DEB, RPM, архив TGZ или Docker-образ.

Руководство пользователя

Предисловие

Добро пожаловать в мир Tarantool! Сейчас вы читаете «Руководство пользователя». Мы советуем начинать именно с него, а затем переходить к «Справочникам», если вам понадобятся более подробные сведения.

Как пользоваться документацией

Для начала можно установить и запустить Tarantool, используя Docker-контейнер, менеджер пакетов или онлайн-сервер Tarantool’а http://try.tarantool.org. В любом случае для пробы можно сделать вводные упражнения из главы 2 «Руководство для начинающих». Если хотите получить практический опыт, переходите к Практическим заданиям после работы с главой 2.

В главе 3 «Функциональность СУБД» рассказано о возможностях Tarantool’а как NoSQL СУБД, а в главе 4 «Сервер приложений» – о возможностях Tarantool’а как сервера приложений Lua.

Глава 5 «Администрирование серверной части» и Глава 6 «Репликация» предназначены в первую очередь для системных администраторов.

Глава 7 «Коннекторы» актуальна только для тех пользователей, которые хотят устанавливать соединение с Tarantool’ом с помощью программ на других языках программирования (например C, Perl или Python) – для прочих пользователей эта глава неактуальна.

Глава 8 «Вопросы и ответы» содержит ответы на некоторые часто задаваемые вопросы о Tarantool’е.

Опытным же пользователям будут полезны «Справочники», «Руководство участника проекта» и комментарии в исходном коде.

Как связаться с сообществом разработчиков Tarantool’а

Оставить сообщение о найденных дефектах или сделать запрос на новые функции можно тут: http://github.com/tarantool/tarantool/issues

Пообщаться напрямую с командой разработки Tarantool’а можно в telegram или на форумах (англоязычном или русскоязычном).

Условные обозначения, используемые в руководстве

В квадратные скобки [ и ] включается синтаксис необязательных элементов.

Две точки подряд .. означают, что предыдущие токены могут повторяться.

Вертикальная черта | означает, что предыдущий и последующий токены представляют собой взаимоисключающие альтернативы.

Функциональность СУБД

В данной главе мы рассмотрим основные понятия при работе с Tarantool’ом в качестве системы управления базой данных.

Эта глава состоит из следующих разделов:

Модель данных

В этом разделе описывается то, как в Tarantool’е организовано хранение данных и какие операции с данными он поддерживает.

Если вы пробовали создать базу данных, как предлагается в упражнениях в «Руководстве для начинающих», то ваша тестовая база данных выглядит следующим образом:

_images/data_model.png

Спейсы

Спейс – с именем „tester“ в нашем примере – это контейнер.

Когда Tarantool используется для хранения данных, всегда существует хотя бы один спейс. У каждого спейса есть уникальное имя, указанное пользователем. Кроме того, пользователь может указать уникальный числовой идентификатор, но обычно Tarantool назначает его автоматически. Наконец, в спейсе всегда есть движок: memtx (по умолчанию) – in-memory движок, быстрый, но ограниченный в размере, или vinyl – дисковый движок для огромного количества данных.

Спейс – это контейнер для кортежей. Для работы ему необходим первичный индекс. Также возможно использование вторичных индексов.

Кортежи

Кортеж играет такую же роль, как “строка” или “запись”, а компоненты кортежа (которые мы называем “полями”) играют такую же роль, что и “столбец” или “поле записи”, не считая того, что:

  • поля могут представлять собой композитные структуры, такие как таблицы типа массива или ассоциативного массива,
  • полям не нужны имена.

В любом кортеже может быть любое количество полей, и это могут быть поля разных типов. Идентификатором поля является его номер, начиная с 1 (в Lua и других языках с индексацией с 1) или с 0 (в PHP или C/C++). Например, 1 или 0 могут использоваться в некоторых контекстах для обозначения первого поля кортежа.

Количество кортежей в спейсе не ограничено.

Кортежи в Tarantool’е хранятся в виде массивов MsgPack.

Когда Tarantool выводит значение в кортеже в консоль, используется формат YAML, например: [3, 'Ace of  Base', 1993].

Индексы

Индекс – это совокупность значений ключей и указателей.

Как и для спейсов, индексам следует указать имена, а Tarantool определит уникальный числовой идентификатор («ID индекса»).

У индекса всегда есть определенный тип. Тип индекса по умолчанию – „TREE“. Все движки Tarantool’а предоставляют TREE-индексы, которые могут индексировать уникальные и неуникальные значения, поддерживают поиск по компонентам ключа, сравнение ключей и упорядоченные результаты. Кроме того, движок memtx поддерживает следующие индексы: HASH, RTREE и BITSET.

Индекс может быть многокомпонентным, то есть можно объявить, что ключ индекса состоит из двух или более полей в кортеже в любом порядке. Например, для обычного TREE-индекса максимальное количество частей равно 255.

Индекс может быть уникальным, то есть можно объявить, что недопустимо дважды задавать одно значение ключа.

Первый индекс, определенный для спейса, называется первичный индекс. Он должен быть уникальным. Все остальные индексы называются вторичными индексами, они могут строиться по неуникальным значениям.

Индекс может содержать идентификаторы полей кортежа и их предполагаемые типы (см. допустимые типы индексированных полей).

Примечание

Рекомендуется проектировать модель данных так, чтобы первичные ключи были первыми полями в котреже, чтобы их было быстрее сравнивать.

В нашем примере для начала определяем первичный индекс (под названием „primary“) по полю №1 каждого кортежа:

tarantool> i = s:create_index('primary', {type = 'hash', parts = {{field = 1, type = 'unsigned'}}}

Смысл в том, что поле №1 должно существовать и содержать целое число без знака для всех кортежей в спейсе „tester“. Тип индекса – „hash“, поэтому значения в поле №1 должны быть уникальными, поскольку ключи в HASH-индексах уникальны.

После этого мы определим вторичный индекс (под названием „secondary“) по полю №2 каждого кортежа:

tarantool> i = s:create_index('secondary', {type = 'tree', parts = {2, 'string'}})

Смысл в том, что поле №2 должно существовать и содержать строку для всех кортежей в спейсе „tester“. Тип индекса – „tree“, поэтому значения в поле №2 не должны быть уникальными, поскольку ключи в TREE-индексах могут не быть уникальными.

Примечание

Определения спейса и определения индексов хранятся в системных спейсах Tarantool’а _space и _index соответственно (для получения подробной информации см. справочник по вложенному модулю box.space).

Можно добавлять, опускать или изменять определения во время исполнения кода с некоторыми ограничениями. Более подробно о синтаксисе см. в справочнике по модулю box.

Подробнее об операциях с индексом читайте здесь.

Типы данных

Tarantool представляет собой базу данных и сервер приложений одновременно. Следовательно, разработчик часто работает с двумя наборами типов: типы языка программирования (например, Lua) и типы формата хранилища Tarantool (MsgPack).

Lua в сравнении с MsgPack
Скалярный / составной MsgPack-тип   Lua-тип Пример значения
скалярный nil «nil» (нулевое значение) msgpack.NULL
скалярный boolean (логический) «boolean» (логическое значение) true
скалярный string (строка) «string» (строка) „A B C“
скалярный integer (целое число) «number» (число) 12345
скалярный double (числа с двойной точностью) «number» (число) 1,2345
составной map (ассоциативный массив) «table» (таблица со строковыми ключами) {„a“: 5, „b“: 6}
составной array (массив) «table» (таблица с целочисленными ключами) [1, 2, 3, 4, 5]
составной array (массив) tuple («cdata») (кортеж) [12345, „A B C“]

В языке Lua тип nil (нулевой) может иметь только одно значение, также называемое nil (отображаемое как null в командной строке Tarantool’а, поскольку значения выводятся в формате YAML). Нулевое значение можно сравнивать со значениями любых типов с помощью операторов == (равен) или ~= (не равен), но никакие другие операции для нулевых значений не доступны. Нулевые значения также нельзя использовать в Lua-таблицах; вместо нулевого значения в таком случае можно указать msgpack.NULL

Тип boolean (логический) может иметь только значения true или false.

Тип string (строка) представляет собой последовательность байтов переменной длины, обычно представленную буквенно-цифровые символы в одинарных кавычках. Как в Lua, так и в MsgPack строки рассматриваются как бинарные данные без попыток определить набор символов строки или выполнить преобразование строки – кроме случаев, когда есть опциональное сравнение символов. Таким образом, обычно сортировка и сравнение строк выполняются побайтово, не применяя дополнительных правил сравнения символов. (Пример: числа упорядочены по их положению на числовой прямой, поэтому 2345 больше, чем 500; а строки упорядочены по кодировке первого байта, затем кодировке второго байта и так далее, таким образом, „2345“ меньше, чем „500“.)

В языке Lua тип number (число) – это число с плавающей запятой двойной точности, но в Tarantool’е можно использовать как целые числа, так и числа с плавающей запятой. Tarantool по возможности сохраняет числа языка Lua в виде чисел с плавающей запятой, если числовое значение содержит десятичную запятую или если оно очень велико (более 100 триллионов = 1e14). В противном случае, Tarantool сохраняет такое значение в виде целого числа. Чтобы даже очень большие величины гарантированно обрабатывались как целые числа, используйте функцию tonumber64, либо приписывайте в конце суффикс LL (Long Long) или ULL (Unsigned Long Long). Вот примеры записи чисел в обычном представлении, экспоненциальном, с суффиксом ULL и с использованием функции tonumber64: -55, -2.7e+20, 100000000000000ULL, tonumber64('18446744073709551615').

В Lua tables (таблицы) со строковыми ключами хранятся как ассоциативные массивы в MsgPack; Lua-таблицы с целочисленными ключами, начиная с 1, хранятся как массивы в MsgPack. Нулевые значения нельзя использовать в Lua-таблицах; вместо нулевого значения в таком случае можно указать msgpack.NULL

Тип tuple (кортеж) представляет собой легкую ссылку на массив MsgPack, который хранится в базе данных. Это особый тип (cdata), чтобы избежать конвертации в Lua-таблицу при выборке данных. Некоторые функции могут возвращать таблицы с множеством кортежей. Примеры с кортежами см. в box.tuple.

Примечание

Tarantool использует формат MsgPack для хранения в базе данных переменной длины. Поэтому, например, для наименьшего числа требуется только один байт, но для наибольшего числа требуется девять байтов.

Примеры запроса вставки с разными типами данных:

tarantool> box.space.K:insert{1,nil,true,'A B C',12345,1.2345}
---
- [1, null, true, 'A B C', 12345, 1.2345]
...
tarantool> box.space.K:insert{2,{['a']=5,['b']=6}}
---
- [2, {'a': 5, 'b': 6}]
...
tarantool> box.space.K:insert{3,{1,2,3,4,5}}
---
- [3, [1, 2, 3, 4, 5]]
...
Типы индексированных полей

Индексы ограничивают значения, которые может содержать MsgPack в Tarantool’е. Вот почему, например, тип „unsigned“ (без знака) представляет собой отдельный тип индексированного поля в сравнении с типом данных ‘integer’ (целое число) в MsgPack: оба содержат значения с целыми числами, но индекс „unsigned“ содержит только неотрицательные целые числовые значения, а индекс ‘integer’ содержит все целые числовые значения.

Вот как типы индексированных полей в Tarantool’е соответствуют типам данных MsgPack.

Тип индексированного поля Тип данных MsgPack
(и возможные значения)
Тип индекса Примеры
unsigned (без знака – может также называться ‘uint’ или ‘num’, но ‘num’ объявлен устаревшим) integer (целое число в диапазоне от 0 до 18 446 744 073 709 551 615, т.е. около 18 квинтиллионов) TREE, BITSET или HASH 123456
integer (целое число – может также называться ‘int’) integer (целое число в диапазоне от -9 223 372 036 854 775 808 до 18 446 744 073 709 551 615) TREE или HASH -2^63
number

integer (целое число в диапазоне от -9 223 372 036 854 775 808 до 18 446 744 073 709 551 615)

double (число с плавающей запятой с одинарной точностью или с двойной точностью)

TREE или HASH

1,234

-44

1,447e+44

string (строка – может также называться ‘str’) string (строка – любая последовательность октетов до максимальной длины) TREE, BITSET или HASH

‘A B C’

‘65 66 67’

boolean bool (логический – true или false) TREE или HASH true
array array (массив – список чисел, который представляет собой точки в геометрической фигуре) RTREE

{10, 11}

{3, 5, 9, 10}

scalar

bool (логический – true или false)

integer (целое число в диапазоне от -9 223 372 036 854 775 808 до 18 446 744 073 709 551 615)

double (число с плавающей запятой с одинарной точностью или с двойной точностью)

string (строковое значение, т.е. любая последовательность октетов)

Примечание: в сочетании различных типов порядок будет следующим: логические значения, затем числовые, затем строковые.

TREE или HASH

true

-1

1,234

‘’

‘ру’

Сортировка

По умолчанию, когда Tarantool сравнивает строки, он использует то, что мы называем «бинарной» сортировкой. Единственный фактор, который учитывается, это числовое значение каждого байта в строке. Таким образом, если строка кодируется по ASCII или UTF-8, то 'A' < 'B' < 'a', поскольку в кодировке „A“ (что раньше называлось «значение ASCII») соответствует 65, „B“ – 66, а „a“ – 98. Бинарная сортировка подходит лучше всего для быстрого детерминированного простого обслуживания и поиска с помощью индексов Tarantool’а.

Однако если необходимо распределение, как в телефонных справочниках и словарях, то вам нужна опциональная сортировка Tarantool’а – unicode и unicode_ci – которые обеспечивают 'a' < 'A' < 'B' и 'a' = 'A' < 'B' соответственно.

Опциональная сортировка использует распределение в соответствии с Таблицей сортировки символов Юникода по умолчанию (DUCET) и правилами, указанными в Техническом стандарте Юникода №10 – Алгоритм сортировки по Юникоду (Unicode® Technical Standard #10 Unicode Collation Algorithm (UTS #10 UCA)). Единственное отличие между двумя сортировками – вес:

  • сортировка unicode принимает во внимание уровни веса L1, L2 и L3 (уровень = „tertiary“, третичный),
  • сортировка unicode_ci принимает во внимание только вес L1 (уровень = „primary“, первичный), поэтому, например, „a“ = „A“ = „á“ = „Á“.

Для примера возьмем некоторые русские слова:

'ЕЛЕ'
'елейный'
'ёлка'
'еловый'
'елозить'
'Ёлочка'
'ёлочный'
'ЕЛь'
'ель'

…и покажем разницу в упорядочении и выборке по индексу:

  • с сортировкой по unicode:

    tarantool> box.space.T:create_index('I', {parts = {{1,'str', collation='unicode'}}})
    ...
    tarantool> box.space.T.index.I:select()
    ---
    - - ['ЕЛЕ']
      - ['елейный']
      - ['ёлка']
      - ['еловый']
      - ['елозить']
      - ['Ёлочка']
      - ['ёлочный']
      - ['ель']
      - ['ЕЛь']
    ...
    tarantool> box.space.T.index.I:select{'ЁлКа'}
    ---
    - []
    ...
    
  • с сортировкой по unicode_ci:

    tarantool> box.space.T:create_index('I', {parts = {{1,'str', collation='unicode_ci'}}})
    ...
    tarantool> box.space.S.index.I:select()
    ---
    - - ['ЕЛЕ']
      - ['елейный']
      - ['ёлка']
      - ['еловый']
      - ['елозить']
      - ['Ёлочка']
      - ['ёлочный']
      - ['ЕЛь']
    ...
    tarantool> box.space.S.index.I:select{'ЁлКа'}
    ---
    - - ['ёлка']
    ...
    

Фактически хорошая сортировка включает в себя гораздо больше, чем простые примеры эквивалентности заглавных и строчных букв, а также наличие или отсутствие диакритических знаков в алфавитах. Учитываются также варианты написания одного и того же символа, системы письменности без алфавита и специальные правила, которые применяются в отношении сочетания символов.

Последовательности

Последовательность – это генератор упорядоченных значений целых чисел.

Как и для спейсов и индексов, для последовательностей следует указать имена, а Tarantool определит уникальный числовой идентификатор («ID последовательности»).

Кроме того, можно указать несколько параметров при создании новой последовательности. Параметры определяют, какое значение будет генерироваться при использовании последовательности.

Параметры для box.schema.sequence.create()
Имя параметра Тип и значение Значение по умолчанию Примеры
start (начало) Целое число. Значение генерируется, когда последовательность используется впервые 1 start=0
min (мин) Целое число. Ниже указанного значения не могут генерироваться 1 min=-1000
max (макс) Целое число. Выше указанного значения не могут генерироваться 9 223 372 036 854 775 807 max=0
cycle (цикл) Логическое значение. Если значения не могут быть сгенерированы, начинать ли заново false (ложь) cycle=true
cache (кэш) Целое число. Количество значений для хранения в кэше 0 cache=0
step (шаг) Целое число. Что добавить к предыдущему сгенерированному значению, когда генерируется новое значение 1 step=-1
if_not_exists (если отсутствует) Логическое значение. Если выставлено в true (истина) и существует последовательность с таким именем, то игнорировать другие опции и использовать текущие значения false (ложь) if_not_exists=true

Существующую последовательность можно изменять, опускать, сбрасывать, заставить сгенерировать новое значение или ассоциировать с индексом.

Для первоначального примера сгенерируем последовательность под названием „S“.

tarantool> box.schema.sequence.create('S',{min=5, start=5})
---
- step: 1
  id: 5
  min: 5
  cache: 0
  uid: 1
  max: 9223372036854775807
  cycle: false
  name: S
  start: 5
...

В результате видим, что в новой последовательность есть все значения по умолчанию, за исключением указанных min и start.

Затем получаем следующее значение с помощью функции next().

tarantool> box.sequence.S:next()
---
- 5
...

Результат точно такой же, как и начальное значение. Если мы снова вызовем next(), то получим 6 (потому что предыдущее значение плюс значение шага составит 6) и так далее.

Затем создадим новую таблицу и скажем, что ее первичный ключ можно получить из последовательности.

tarantool> s=box.schema.space.create('T');s:create_index('I',{sequence='S'})
---
...

Затем вставим кортеж, не указывая значение первичного ключа.

tarantool> box.space.T:insert{nil,'other stuff'}
---
- [6, 'other stuff']
...

В результате имеем новый кортеж со значением 6 в первом поле. Такой способ организации данных, когда система автоматически генерирует значения для первичного ключа, иногда называется «автоинкрементным» (т.е. с автоматическим увеличением) или «по идентификатору».

Для получения подробной информации о синтаксисе и методах реализации см. справочник по box.schema.sequence.

Персистентность

В Tarantool’е обновления базы данных записываются в так называемые файлы журнала упреждающей записи (WAL-файлы). Это обеспечивает персистентность данных. При отключении электроэнергии или случайном завершении работы экземпляра Tarantool’а данные в оперативной памяти теряются. В такой ситуации WAL-файлы используются для восстановления данных так: Tarantool прочитывает WAL-файлы и повторно выполняет запросы (это называется «процессом восстановления»). Можно изменить временные настройки метода записи WAL-файлов или отключить его с помощью wal_mode.

Tarantool также сохраняет ряд файлов со статическими снимками данных (snapshots). Файл со снимком – это дисковая копия всех данных в базе на какой-то момент. Вместо того, чтобы зачитывать все WAL-файлы, появившиеся с момента создания базы, Tarantool в процессе восстановления может загрузить самый свежий снимок и затем зачитать только те WAL-файлы, которые были сделаны с момента сохранения снимка. После создания новых файлов, старые WAL-файлы могут быть удалены в целях экономии места на диске.

Чтобы принудительно создать файл со снимком, можно использовать запрос box.snapshot() в Tarantool’е. Чтобы включить автоматическое создание файлов со снимком, можно использовать демон создания контрольных точек Tarantool’а. Демон создания контрольных точек определяет интервалы для принудительного создания контрольных точек. Он обеспечивает синхронизацию и сохранение на диск образов движков базы данных (как memtx, так и vinyl), а также автоматически удаляет старые WAL-файлы.

Файлы со снимками можно создавать, даже если WAL-файлы отсутствуют.

Примечание

Движок memtx регулярно создает контрольные точки с интервалом, указанным в настройках демона создания контрольных точек.

Движок vinyl постоянно сохраняет состояние в контрольной точке в фоновом режиме.

Для получения более подробной информации о методе записи WAL-файлов и процессе восстановления см. раздел Внутренняя реализация.

Операции

Операции с данными

Tarantool поддерживает следующие основные операции с данными:

  • пять операций по изменению данных (INSERT, UPDATE, UPSERT, DELETE, REPLACE) и
  • одну операцию по выборке данных (SELECT).

Все они реализованы в виде функций во вложенном модуле box.space.

Примеры:

  • INSERT: добавить новый кортеж к спейсу „tester“.

    Первое поле, field[1], будет 999 (тип MsgPack – integer, целое число).

    Второе поле, field[2], будет „Taranto“ (тип MsgPack – string, строка).

    tarantool> box.space.tester:insert{999, 'Taranto'}
    
  • UPDATE: обновить кортеж, изменяя поле field[2].

    Оператор «{999}» со значением, которое используется для поиска поля, соответствующего ключу в первичном индексе, является обязательным, поскольку в запросе update() должен быть оператор, который указывает уникальный ключ, в данном случае – field[1].

    Оператор «{{„=“, 2, „Tarantino“}}» указывает, что назначение нового значения относится к field[2].

    tarantool> box.space.tester:update({999}, {{'=', 2, 'Tarantino'}})
    
  • UPSERT: обновить или вставить кортеж, снова изменяя поле field[2].

    Синтаксис upsert() похож на синтаксис update(). Однако логика выполнения двух запросов отличается. UPSERT означает UPDATE или INSERT, в зависимости от состояния базы данных. Кроме того, выполнение UPSERT откладывается до коммита транзакции, поэтому в отличие от``update()``, upsert() не возвращает данные.

    tarantool> box.space.tester:upsert({999, 'Taranted'}, {{'=', 2, 'Tarantism'}})
    
  • REPLACE: заменить кортеж, добавляя новое поле.

    Это действие также можно выполнить с помощью запроса update(), но обычно запрос update() более сложен.

    tarantool> box.space.tester:replace{999, 'Tarantella', 'Tarantula'}
    
  • SELECT: провести выборку кортежа.

    Оператор «{999}» все еще обязателен, хотя в нем не должен упоминаться первичный ключ.

    tarantool> box.space.tester:select{999}
    
  • DELETE: удалить кортеж.

    В этом примере мы определяем поле, соответствующее ключу в первичном индексе.

    tarantool> box.space.tester:delete{999}
    

Подводя итоги по примерам:

  • Функции insert и replace принимают кортеж (где первичный ключ – это часть кортежа).
  • Функция upsert принимает кортеж (где первичный ключ – это часть кортежа), а также операции по обновлению.
  • Функция delete принимает полный ключ любого уникального индекса (первичный или вторичный).
  • Функция update принимает полный ключ любого уникального индекса (первичный или вторичный), а также операции к выполнению.
  • Функция select принимает любой ключ: первичный/вторичный, уникальный/неуникальный, полный/часть.

Для получения более подробной информации по использованию операций с данными см. справочник по box.space.

Примечание

Помимо Lua можно использовать коннекторы к Perl, PHP, Python или другому языку программирования. Клиент-серверный протокол открыт и задокументирован. См. БНФ с комментариями.

Операции с индексами

Операции с индексами производятся автоматически. Если запрос по манипулированию данными меняет данные в кортеже, то меняются и ключи в индексе для данного кортежа.

Простая операция по созданию индекса, которую мы рассматривали ранее, выглядит следующим образом:

box.space.space-name:create_index('index-name')

По умолчанию, при этом создается TREE-индекс по первому полю для всех кортежей (обычно его называют «Field#1»). Предполагается, что индексируемое поле является числовым.

Вот простой SELECT-запрос, который мы рассматривали ранее:

box.space.space-name:select(value)

Такой запрос ищет отдельный кортеж по первичному индексу. Поскольку первичный индекс всегда уникален, то данный запрос вернет не более одного кортежа. Можно также вызвать select() без аргументов, чтобы вернуть все кортежи.

Продолжим работу со спейсом „tester“, созданным в упражнениях из «Руководства для начинающих», но сначала его немного модифицируем:

tarantool> box.space.tester:format({
         > {name = 'id', type = 'unsigned'},
         > {name = 'band_name', type = 'string'},
         > {name = 'year', type = 'unsigned'},
         > {name = 'rate', type = 'unsigned', is_nullable=true}})
---
...

Добавим рейтинг „rate“ кортежам #1 и #2:

tarantool> box.space.tester:update(1, {{'=', 4, 5}})
---
- [1, 'Roxette', 1986, 5]
...
tarantool> box.space.tester:update(2, {{'=', 4, 4}})
---
- [2, 'Scorpions', 2015, 4]
...

И создадим еще один кортеж:

tarantool> box.space.tester:insert({4, 'Roxette', 2016, 3})
---
- [4, 'Roxette', 2016, 3]
...

Существующие вариации SELECT:

  1. Помимо условия равенства, при поиске могут использоваться и другие условия сравнения.
tarantool> box.space.tester:select(1, {iterator = 'GT'})
---
- - [2, 'Scorpions', 2015, 4]
  - [3, 'Ace of Base', 1993]
  - [4, 'Roxette', 2016, 3]
...

Можно использовать следующие операторы сравнения: LT (меньше), LE (меньше или равно), EQ (равно, результаты отсортированы в порядке возрастания по ключу), REQ (равно, результаты отсортированы в порядке убывания по ключу), GE (больше или равно), GT (больше). Сравнения имеют смысл только для индексов типа „TREE“.

Этот вариант поиска может вернуть более одного кортежа. В таком случае кортежи будут отсортированы в порядке убывания по ключу (если использовался оператор LT, LE или REQ), либо в порядке возрастания (во всех остальных случаях).

  1. Поиск может производиться по вторичному индексу.

При поиске по первичному индексу имя индекса можно не указывать. При поиске же по вторичному индексу имя индекса указывать необходимо.

tarantool> box.space.tester:create_index('secondary', {parts = {{field=3, type='unsigned'}}})
---
- unique: true
  parts:
  - type: unsigned
    is_nullable: false
    fieldno: 3
  id: 2
  space_id: 512
  type: TREE
  name: secondary
...
tarantool> box.space.tester.index.secondary:select({1993})
---
- - [3, 'Ace of Base', 1993]
...
  1. Поиск можно осуществить по некоторым частям ключа, используя его префикс. Обратите внимание, что частичный поиск по ключу доступен только в TREE индексах.
-- Создаем индекс, состоящий из трех частей
tarantool> box.space.tester:create_index('tertiary', {parts = {{field = 2, type = 'string'}, {field=3, type='unsigned'}, {field=4, type='unsigned'}}})
---
- unique: true
  parts:
  - type: string
    is_nullable: false
    fieldno: 2
  - type: unsigned
    is_nullable: false
    fieldno: 3
  - type: unsigned
    is_nullable: true
    fieldno: 4
  id: 6
  space_id: 513
  type: TREE
  name: tertiary
...
-- Выполняем частичный поиск
tarantool> box.space.tester.index.tertiary:select({'Scorpions', 2015})
---
- - [2, 'Scorpions', 2015, 4]
...
  1. Поиск может производиться по всем полям через запись в виде таблицы:
tarantool> box.space.tester.index.tertiary:select({'Roxette', 2016, 3})
---
- - [4, 'Roxette', 2016, 3]
...

либо же по одному полю (в этом случае используется таблица или скалярное значение):

tarantool> box.space.tester.index.tertiary:select({'Roxette'})
---
- - [1, 'Roxette', 1986, 5]
  - [4, 'Roxette', 2016, 3]
...
Работа с BITSET и RTREE

Примеры BITSET:

tarantool> box.schema.space.create('bitset_example')
tarantool> box.space.bitset_example:create_index('primary')
tarantool> box.space.bitset_example:create_index('bitset',{unique=false,type='BITSET', parts={2,'unsigned'}})
tarantool> box.space.bitset_example:insert{1,1}
tarantool> box.space.bitset_example:insert{2,4}
tarantool> box.space.bitset_example:insert{3,7}
tarantool> box.space.bitset_example:insert{4,3}
tarantool> box.space.bitset_example.index.bitset:select(2, {iterator='BITS_ANY_SET'})

Мы получим следующий результат:

---
- - [3, 7]
  - [4, 3]
...

поскольку (7 AND 2) не равно 0 и (3 AND 2) не равно 0.

Примеры RTREE:

tarantool> box.schema.space.create('rtree_example')
tarantool> box.space.rtree_example:create_index('primary')
tarantool> box.space.rtree_example:create_index('rtree',{unique=false,type='RTREE', parts={2,'ARRAY'}})
tarantool> box.space.rtree_example:insert{1, {3, 5, 9, 10}}
tarantool> box.space.rtree_example:insert{2, {10, 11}}
tarantool> box.space.rtree_example.index.rtree:select({4, 7, 5, 9}, {iterator = 'GT'})

Мы получим следующий результат:

---
- - [1, [3, 5, 9, 10]]
...

поскольку прямоугольник с углами в координатах 4,7,5,9 лежит целиком внутри прямоугольника с углами в координатах 3,5,9,10.

Кроме того, есть операции с итераторами с индексом. Их можно использовать только с кодом на языках Lua и C/C++. Итераторы с индексом предназначены для обхода индексов по одному ключу за раз, поскольку используют особенности каждого типа индекса, например оценка логических выражений при обходе BITSET-индексов или обход TREE-индексов в порядке по убыванию.

Полный список операций над индексами, таких как alter() (изменение индекса) и drop() (удаление индекса), приводится в справочнике для Вложенный модуль box.index.

Факторы сложности

Во вложенных модулях box.space и Вложенный модуль box.index содержится информация о том, как факторы сложности могут повлиять на использование каждой функции.

Фактор сложности Эффект
Размер индекса Количество ключей в индексе равно количеству кортежей в наборе данных. В случае с TREE-индексом: с ростом количества ключей увеличивается время поиска, хотя зависимость здесь, конечно же, не линейная. В случае с HASH-индексом: с ростом количества ключей увеличивается объем оперативной памяти, но количество низкоуровневых шагов остается примерно тем же.
Тип индекса Как правило, поиск по HASH-индексу работает быстрее, чем по TREE-индексу, если в спейсе более одного кортежа.
Количество обращений к индексам

Обычно для выборки значений одного кортежа используется только один индекс. Но при обновлении значений в кортеже требуется N обращений, если в спейсе N индексов.

Примечание по движку базы данных: Vinyl отклоняет такой доступ, если обновление не затрагивает поля вторичного индекса. Таким образом, этот фактор сложности влияет только на memtx, поскольку он всегда создает копию всего кортежа при каждом обновлении.

Количество обращений к кортежам Некоторые запросы, например SELECT, могут возвращать несколько кортежей. Как правило, это наименее важный фактор из всех.
Настройки WAL Важным параметром для записи в WAL является wal_mode. Если запись в WAL отключена или задана запись с задержкой, но этот фактор не так важен. Если же запись в WAL производится при каждом запросе на изменение данных, то при каждом таком запросе приходится ждать, пока отработает обращение к более медленному диску, и данный фактор становится важнее всех остальных.

Транзакции

Транзакции в Tarantool’е происходят в файберах в одном потоке. Вот почему Tarantool дает гарантию атомарности выполнения. На этом следует сделать акцент.

Потоки, файберы и передача управления

Как Tarantool выполняет основные операции? Для примера возьмем такой запрос:

tarantool> box.space.tester:update({3}, {{'=', 2, 'size'}, {'=', 3, 0}})

Это эквивалентно следующему SQL-выражению (оно работает с таблицей, где первичные ключи в field[1]):

UPDATE tester SET "field[2]" = 'size', "field[3]" = 0 WHERE "field[1]" = 3

Предположим, что этот запрос Tarantool получил по сети, — тогда три потока операционной системы будут обрабатывать этот запрос:

  1. Сетевой поток на стороне сервера получает запрос, разбирает выражение, проверяет его на правильность и преобразует его в специальную структуру — сообщение, которое содержит готовый для исполнения запрос и его опции.

  2. Сетевой поток отправляет это сообщение в поток обработки транзакций с помощью шины передачи сообщений без блокировок. Lua-программы выполняются непосредственно в потоке обработки транзакций и не требуют разбора и подготовки.

    Чтобы найти нужный кортеж, поток обработки транзакций использует индекс на поле первичного ключа field[1]. Он проверяет, что этот кортеж можно обновить (вряд ли что-то пойдет не так, если мы всего лишь меняем значение не индексированного поля).

  3. Поток обработки транзакций отправляет сообщение в поток упреждающей записи в журнал (WAL) для коммита транзакции. По завершении поток WAL отправляет результат транзакции — COMMIT или ROLLBACK — в поток обработки транзакций, который передает его сетевому потоку, а тот возвращает результат клиенту.

Обратите внимание, что в Tarantool’е есть только один поток обработки транзакций. Некоторые уже привыкли к мысли, что потоков для обработки данных в базе данных может быть много (например, поток №1 читает данные из строки №x, в то время как поток №2 записывает данные в столбец №y). В случае с Tarantool’ом такого не происходит. Доступ к базе есть только у потока обработки транзакций, и на каждый экземпляр Tarantool’а есть только один такой поток.

Как и любой другой поток Tarantool’а, поток обработки транзакций может управлять множеством файберов. Файбер – это набор команд, среди которых могут быть и сигналы «передачи управления». Поток обработки транзакций выполняет все команды, пока не увидит такой сигнал, и тогда он переключается на выполнение команд из другого файбера. Например, таким образом поток обработки транзакций сначала выполняет чтение данных из строки №x для файбера №1, а затем выполняет запись в строку №y для файбера №2.

Передача управления необходима, в противном случае, поток обработки транзакции заклинит на одном файбере. Есть два типа передачи управления:

  • неявная передача управления: каждая операция по изменению данных или доступ к сети вызывают неявную передачу управления, а также каждое выражение, которое проходит через клиент Tarantool’а, вызывает неявную передачу управления.
  • явная передача управления: в Lua-функции можно (и нужно) добавить выражения «передачи управления» для предотвращения захвата ЦП. Это называется кооперативной многозадачностью.

Кооперативная многозадачность

Кооперативная многозадачность означает, что если запущенный файбер намеренно не передаст управление, он не вытесняется каким-либо другим файбером. Но запущенный файбер намеренно передает управление, когда обнаруживает “точку передачи управления”: коммит транзакции, вызов операционной системы или запрос явной «передачи управления». Любой вызов системы, который может блокировать файбер, будет производиться асинхронно, а запущенный файбер, который должен ожидать системного вызова, будет вытеснен так, что другой готовый к работе файбер занимает его место и становится запущенным файбером.

Такая модель позволяет отказаться от программных блокировок, поскольку кооперативная многозадачность обеспечивает отсутствие борьбы за ресурс, гонки потоков и проблем с согласованностью данных. Добиться этого довольно просто: не использовать явную или неявную передачу управления в критических секциях, и никто не сможет вмешаться в выполнение кода.

При небольших запросах, таких как простые UPDATE, INSERT, DELETE или SELECT, происходит справедливое планирование файберов: немного времени требуется на обработку запроса, планирование записи на диск и передачу управления на файбер, обслуживающий следующего клиента.

Однако функция может выполнять сложные расчеты или может быть написана так, что управление не передается в течение длительного времени. Это может привести к несправедливому планированию, когда отдельный клиент перекрывает работу остальной системы, или к явным задержкам в обработке запросов. Автору функции следует не допускать таких ситуаций.

Транзакции

В отсутствие транзакций любая функция, в которой есть точки передачи управления, может видеть изменения в состоянии базы данных, вызванные вытесняющими файберами. Составные транзакции предназначены для изоляции: каждая транзакция видит постоянное состояние базы данных и делает атомарные коммиты изменений. Во время коммита происходит передача управления, а все транзакционные изменения записываются в журнал упреждающей записи в отдельный пакет. Или, при необходимости, можно откатить изменения – полностью или на определенную точку сохраниения.

В Tarantool транзакции изолированы полностью — на уровне serializable (упорядочиваемость) с оговоркой: «если нет сбоев при записи в WAL». В случае такого сбоя, например при переполнении дискового пространства, транзакции изолированы на уровне read uncommitted (чтение незафиксированных данных).

In vynil, to implement isolation Tarantool uses a simple optimistic scheduler: the first transaction to commit wins. If a concurrent active transaction has read a value modified by a committed transaction, it is aborted.

Кооперативный планировщик обеспечивает, что в отсутствие передачи управления составная транзакция не вытесняется, поэтому никогда не прерывается. Таким образом, понимание передачи управления необходимо для написания кода без прерываний.

Иногда при тестировании механизма транзакций в Tarantool можно заметить, что выдача после box.begin(), но перед любой операцией чтения/записи не приводит к прерыванию, как это должно происходить согласно описанию. Причина в том, что на самом деле box.begin() не запускает транзакцию: это просто метка, указывающая Tarantool запустить транзакцию после некоторого запроса к базе данных, который следует за этим.

In memtx, if an instruction that implies yields, explicit or implicit, is executed during a transaction, the transaction is fully rolled back. In vynil, we use more complex transactional manager that allows yields.

Примечание

На сегодняшний день нельзя смешивать движки базы данных в транзакции.

Правила неявной передачи управления

Единственные запросы явной передачи данных в Tarantool’е отправляют fiber.sleep() и fiber.yield(), но многие другие запросы «неявно» подразумевают передачу управления, поскольку цель Tarantool’а – избежать блокировок.

Запросы к базе данных подразумевают передачу управления тогда и только тогда, когда происходят дисковые операции ввода-вывода. Так как все данные в memtx находятся в оперативной памяти, во время запроса на чтение дискового ввода-вывода не происходит. В vinyl некоторые данные могут находиться не в оперативной памяти, поэтому возможны дисковые операции при чтении (для получения данных с диска) или при записи (потому что может произойти сбой в ожидании освобождения памяти). Запросы на изменение данных и в memtx, и в vinyl должны записываться в WAL, и обычно происходит коммит. Коммит происходит автоматически после каждого запроса в режиме «автоматических коммитов» (autocommit) по умолчанию или в конце транзакции в режиме «транзакция» (transaction), когда пользователь вручную производит коммит, вызывая box.commit(). Поэтому и для memtx, и для vinyl некоторые операции с БД могут подразумевать передачу управления при наличии дискового ввода-вывода.

Многие функции в модулях fio, net_box, console и socket (запросы «ОС» и «сети») передают управление.

Поэтому выполнение отдельных команд, таких как select(), insert(), update() в консоли внутри транзакции, приведет к прерыванию транзакции. Это связано с тем, что после выполнения каждого фрагмента кода в консоли происходит неявная передача управления (yield).

Пример №1

  • Когда движок базы данных - memtx
    В последовательности select() insert() управление передается один раз в конце вставки, что вызвано неявным коммитом; select() ничего не записывает в WAL, поэтому не передает управление.
  • Когда движок базы данных - vinyl
    В последовательности select() insert() управление передается от одного до трех раз: select() может передавать управление, если данных нет в кэше; insert() может передавать управление, пока ожидает доступную память; и при коммите будет неявная передача управления.
  • Последовательность begin() insert() insert() commit() передает управление только при коммите, если движок – memtx, и может передавать управление до 3 раз, если движок – vinyl.

Пример №2

Предположим, что в спейсе ‘tester’ в memtx есть кортежи, в которых третье поле представляет собой положительную сумму в долларах. Начнем транзакцию, снимем со счета из кортежа №1, пополним счет в кортеж №2 и закончим транзакцию, подтвердив изменения.

tarantool> function txn_example(from, to, amount_of_money)
         >   box.begin()
         >   box.space.tester:update(from, {{'-', 3, amount_of_money}})
         >   box.space.tester:update(to,   {{'+', 3, amount_of_money}})
         >   box.commit()
         >   return "ok"
         > end
---
...
tarantool> txn_example({999}, {1000}, 1.00)
---
- "ok"
...

Если wal_mode = ‘none’, то при коммите управление не передается неявно, потому что не идет запись в WAL-файл.

Если задача интерактивная — отправка запросов к серверу и получение ответов — то она подразумевает сетевой ввод-вывод и неявную передачу управления, даже если запрос, который отправляется на сервер, сам по себе не будет запросом на неявную передачу управления. Поэтому такая последовательность

conn.space.test:select{1}
conn.space.test:select{2}
conn.space.test:select{3}

вызывает передачу управления три раза при отправке запросов в сеть и ожидании результатов. На стороне сервера те же самые запросы выполняются в общем порядке, возможно, смешиваясь с другими запросами из сети и локальных файберов. Что-то подобное происходит, если использовать клиент, который работает через telnet, с помощью одного из коннекторов или сторонних модулей MySQL и PostgreSQL или в интерактивном режиме, когда Tarantool используется в качестве клиента.

После того, как файбер передал управление, а затем вернул его, он незамедлительно вызывает testcancel.

Управление доступом

В основном администраторы занимаются вопросами настроек безопасности. Однако обычные пользователи должны хотя бы бегло прочитать этот раздел, чтобы понять, как Tarantool позволяет администраторам не допустить неавторизованный доступ к базе данных и некоторым функциям.

Вкратце:

  • Существует метод, который с помощью паролей проверяет, что пользователи являются теми, за кого себя выдают (“аутентификация”).
  • Существует системный спейс _user, где хранятся имена пользователей и хеши паролей.
  • Существуют функции, чтобы дать определенным пользователям право совершать определенные действия (“права”).
  • Существует системный спейс _priv, где хранятся права. Когда пользователь пытается выполнить операцию, проводится проверка на наличие у него прав на выполнение такой операции (“управление доступом”).

Подробная информация приводится ниже.

Пользователи

Для любой локальной или удаленной программы, работающей с Tarantool’ом, есть текущий пользователь. Если удаленное соединение использует бинарный порт, то текущим пользователем, по умолчанию, будет „guest“ (гость). Если соединение использует порт для административной консоли, текущим пользователем будет „admin“ (администратор). При выполнении скрипта инициализации на Lua, текущим пользователем также будет ‘admin’.

Имя текущего пользователя можно узнать с помощью box.session.user().

Текущего пользователя можно изменить:

  • Для соединения по бинарному порту – с помощью команды протокола AUTH, которая поддерживается большинством клиентов;
  • Для соединения по порту для административной консоли и при выполнении скрипта инициализации на Lua – с помощью box.session.su();
  • Для соединения по бинарному порту, которое вызывает хранимую функцию с помощью команды CALL – если для функции включена настройка SETUID, Tarantool временно заменит текущего пользователя на создателя функции со всеми правами создателя во время выполнения функции.

Пароли

У каждого пользователя (за исключением гостя „guest“) может быть пароль. Паролем является любая буквенно-цифровая строка.

Пароли Tarantool’а хранятся в системном спейсе _user с криптографической хеш-функцией, так что если паролем является ‘x’, хранится хеш-пароль в виде длинной строки, например ‘lL3OvhkIPOKh+Vn9Avlkx69M/Ck=‘. Когда клиент подключается к экземпляру Tarantool’а, экземпляр отправляет случайное значение соль, которое клиент должен сложить вместе с хеш-паролем перед отправкой на экземпляр. Таким образом, изначальное значение ‘x’ никогда не хранится нигде, кроме как в голове самого пользователя, а хешированное значение никогда не передается по сети, кроме как в смешанном с солью виде.

Примечание

Для получения дополнительной информации об алгоритме хеширования паролей (например, для написания нового клиентского приложения), прочтите файл заголовка scramble.h.

Система не дает злоумышленнику определить пароли путем просмотра файлов журнала или слежения за активностью. Это та же система, несколько лет назад внедренная в MySQL, которой оказалось достаточно для объектов со средней степенью безопасности. Тем не менее, администраторы должны предупреждать пользователей, что никакая система не защищена полностью от постоянных длительных атак, поэтому пароли следует охранять и периодически изменять. Администраторы также должны рекомендовать пользователям выбирать длинные неочевидные пароли, но сами пользователи выбирают свои пароли и изменяют их.

Для управления паролями в Tarantool’е есть две функции: box.schema.user.passwd() для изменения пароля пользователя и box.schema.user.password() для получения хеша пароля пользователя.

Владельцы и права

В Tarantool’е одна база данных. Она может называться «box.schema» или «universe». База данных содержит объекты базы данных, включая спейсы, индексы, пользователей, роли, последовательности и функции.

Владелец объекта базы данных – это пользователь, который создал его. Владельцем самой базы данных и объектов, которые изначально были созданы (системные спейсы и пользователи по умолчанию) является „admin“.

У владельцев автоматически есть права на то, что они создают. Владельцы могут поделиться этими правами с другими пользователями или ролями с помощью запросов box.schema.user.grant(). Можно предоставить следующие права:

  • „read“ (чтение), например, разрешить выборку из спейса
  • „write“ (запись), например, разрешить обновление спейса
  • „execute“ (выполнение), например, разрешить вызов функции, или (реже) разрешить использование роли
  • „create“ (создание), например, разрешить выполнение box.schema.space.create (также необходим доступ к определенным системным спейсам)
  • „alter“ (изменение), например, разрешить выполнение box.space.x.index.y:alter (также необходим доступ к определенным системным спейсам
  • „drop“ (удаление), например, разрешить выполнение box.sequence.x:drop (сейчас можно настроить такие права, но они не действуют)
  • „usage“ (использование), например, допустимо ли любое действие, несмотря на другие права (иногда удобно отменить право на использование, чтобы временно заблокировать пользователя, не удаляя ег
  • „session“ (сессия), например, может ли пользователь выполнить подключение „connect“.

Чтобы создавать объекты, у пользователей должны быть права на создание „create“ и хотя бы права на чтение „read“ и запись „write“ в системный спейс с похожим именем (например, на спейс _space, если пользователю необходимо создавать спейсы.

Чтобы получать доступ к объектам, у пользователей должны быть соответствующие права на объект (например, права на выполнение „execute“ на функцию F, если пользователям необходимо выполнить функцию F). См. ниже некоторые примеры предоставления определенных прав, которые может выдать „admin“ или создатель объекта.

Чтобы удалить объект, пользователь должен быть создателем объекта или „admin“. Как владелец всей базы данных, „admin“ может удалить любой объект, в том числе других пользователей.

Чтобы предоставить права пользователю, владелец объекта выполняет команду box.schema.user.grant(). Чтобы отменить права пользователя, владелец объекта выполняет команду box.schema.user.revoke(). В любом случае можно использовать до пяти параметров:

(user-name, privilege, object-type [, object-name [, options]])
  • user-name – это пользователь (или роль), который получит или потеряет права;

  • privilege – это тип прав: „read“, „write“, „execute“, „create“, „alter“, „drop“, „usage“ или „session“ (или список прав, разделенных запятыми);

  • object-type – это любой тип объекта: „space“ (спейс), „index“ (индекс), „sequence“ (последовательность), „function“ (функция), имя роли или „universe“;

  • object-name – это то, на что распространяются права (не указывается, если object-type = „universe“);

  • options – это список параметров, приведенный в скобках, например, {if_not_exists=true|false} (как правило, не указывается, поскольку допускаются значения по умолчанию).

    Все изменения прав пользователя сразу же отражаются на текущих сессиях и на объектах, например, функциях.

Пример предоставления нескольких типов прав одновременно

В данном примере пользователь „admin“ выдает много типов прав на множество объектов пользователю „U“ в едином запросе.

box.schema.user.grant('U','read,write,execute,create,drop','universe')

Примеры предоставления прав на определенные действия

В данных примерах создатель объекта выдает пользователю „U“ минимально необходимые права на определенные действия.

-- Чтобы 'U' мог создавать спейсы:
   box.schema.user.grant('U','create','universe')
   box.schema.user.grant('U','write', 'space', '_schema')
   box.schema.user.grant('U','write', 'space', '_space')
-- Чтобы 'U' мог создавать индексы (подразумевая, что 'U' создал спейс)
   box.schema.user.grant('U','read', 'space', '_space')
   box.schema.user.grant('U','read,write', 'space', '_index')
-- Чтобы 'U' мог создавать индексы в спейсы T (подразумевая, что 'U' не создал спейс T)
   box.schema.user.grant('U','create','space','T')
   box.schema.user.grant('U','read', 'space', '_space')
   box.schema.user.grant('U','write', 'space', '_index')
-- Чтобы 'U' мог изменять индексы в спейсе T (подразумевая, что 'U' не создал индекс)
   box.schema.user.grant('U','alter','space','T')
   box.schema.user.grant('U','read','space','_space')
   box.schema.user.grant('U','read','space','_index')
   box.schema.user.grant('U','read','space','_space_sequence')
   box.schema.user.grant('U','write','space','_index')
-- Чтобы 'U' мог создавать пользователей или роли:
   box.schema.user.grant('U','create','universe')
   box.schema.user.grant('U','read,write', 'space', '_user')
   box.schema.user.grant('U','write','space', '_priv')
-- Чтобы 'U' мог создавать последовательности:
   box.schema.user.grant('U','create','universe')
   box.schema.user.grant('U','read,write','space','_sequence')
-- Чтобы 'U' мог создавать функции:
   box.schema.user.grant('U','create','universe')
   box.schema.user.grant('U','read,write','space','_func')
-- Чтобы 'U' мог выдавать права на созданные им объекты:
   box.schema.user.grant('U','read','space','_user')
-- Чтобы 'U' мог производить выборку или получать данные из спейса под названием 'T'
   box.schema.user.grant('U','read','space','T')
-- Чтобы 'U' мог производить обновление, вставку, удаление или очистку спейса под названием 'T'
   box.schema.user.grant('U','write','space','T')
-- Чтобы 'U' мог выполнять функцию под названием 'F'
   box.schema.user.grant('U','execute','function','F')
-- Чтобы 'U' мог использовать функцию "S:next()" для последовательности под названием S
   box.schema.user.grant('U','read,write','sequence','S')
-- Чтобы 'U' мог использовать функцию "S:set()" или "S:reset()" для последовательности под названием S
  box.schema.user.grant('U','write','sequence','S')

Пример создания пользователей и объектов и последующей выдачи прав

Здесь создадим Lua-функцию, которая будет выполняться от ID пользователя, который является ее создателем, даже если она вызывается другим пользователем.

Для начала создадим два спейса („u“ и „i“) и дадим полный доступ к ним пользователю без пароля („internal“). Затем определим функцию („read_and_modify“), и пользователь без пароля становится создателем функции. Наконец, дадим другому пользователю („public_user“) доступ на выполнение Lua-функций, созданных пользователем без пароля.

box.schema.space.create('u')
box.schema.space.create('i')
box.space.u:create_index('pk')
box.space.i:create_index('pk')

box.schema.user.create('internal')

box.schema.user.grant('internal', 'read,write', 'space', 'u')
box.schema.user.grant('internal', 'read,write', 'space', 'i')
box.schema.user.grant('internal', 'create', 'universe')
box.schema.user.grant('internal', 'read,write', 'space', '_func')

function read_and_modify(key)
  local u = box.space.u
  local i = box.space.i
  local fiber = require('fiber')
  local t = u:get{key}
  if t ~= nil then
    u:put{key, box.session.uid()}
    i:put{key, fiber.time()}
  end
end

box.session.su('internal')
box.schema.func.create('read_and_modify', {setuid= true})
box.session.su('admin')
box.schema.user.create('public_user', {password = 'secret'})
box.schema.user.grant('public_user', 'execute', 'function', 'read_and_modify')

Роли

Роль представляет собой контейнер для прав, которые можно предоставить обычным пользователям. Вместо того, чтобы предоставлять или отменять индивидуальные права, можно поместить все права в роль, а затем назначить или отменить роль.

Информация о роли хранится в спейсе _user, но третье поле кортежа – поле типа – это ‘роль’, а не ‘пользователь’.

В управлении доступом на основе ролей один из главных моментов – это то, что роли могут быть вложенными. Например, роли R1 можно предоставить право типа «роль R2», то есть пользователи с ролью R1 тогда получат все права роли R1 и роли R2. Другими словами, пользователь получает все права, которые предоставляются ролям пользователя напрямую и опосредованно.

Фактически есть два способа предоставить или отменить роль: box.schema.user.grant-or-revoke(имя-пользователя-или-имя-роли,'execute', 'role',имя-роли...) или box.schema.user.grant-or-revoke(имя-пользователя-или-имя-роли,имя-роли...). Рекомендуется использовать второй способ.

Права типов „usage“ и „session“ нельзя предоставить для роли.

Пример

-- Этот пример сработает для пользователя со множеством прав, например, 'admin'
-- или для пользователя с заданной ролью 'super'
-- Создать спейс T с первичным индексом
box.schema.space.create('T')
box.space.T:create_index('primary', {})
-- Создать пользователя U1, чтобы затем можно было заменить текущего пользователя на U1
box.schema.user.create('U1')
-- Создать две роли, R1 и R2
box.schema.role.create('R1')
box.schema.role.create('R2')
-- Предоставить роль R2 для роли R1, а роль R1 пользователю U1 (порядок не имеет значения)
-- Есть два способа предоставить роль, здесь используется более короткий способ
box.schema.role.grant('R1', 'R2')
box.schema.user.grant('U1', 'R1')
-- Предоставить права на чтение/запись на спейс T для роли R2
-- (но не для роли R1 и не пользователю U1)
box.schema.role.grant('R2', 'read,write', 'space', 'T')
-- Изменить текущего пользователя на пользователя U1
box.session.su('U1')
-- Теперь вставка в спейс T сработает, потому что благодаря вложенным ролям,
-- у пользователя U1 есть права на запись в спейс T
box.space.T:insert{1}

Более подробную информацию см. в справочнике по встроенным модулям: box.schema.user.grant() и box.schema.role.grant().

Сессии и безопасность

Сессия – это состояние подключения к Tarantool’у. Она содержит:

  • идентификатор в виде целого числа, определяющий соединение,
  • текущий пользователь, использующий соединение,
  • текстовое описание подключенного узла и
  • локальное состояние сессии, например, переменные и функции на Lua.

В Tarantool’е отдельная сессия может выполнять несколько транзакций одновременно. Каждая транзакция определяется по уникальному идентификатору в виде целого числа, который можно запросить в начале транзакции с помощью box.session.sync().

Примечание

Чтобы отследить все подключения и отключения, можно использовать триггеры соединений и аутентификации.

Триггеры

Триггеры, которые также называют обратными вызовами, представляют собой функции, которые выполняет сервер при наступлении определенных событий.

В Tarantool’е есть четыре типа триггеров:

У всех триггеров есть следующие особенности:

  • Триггеры связывают функцию с событием. Запрос «определить триггер» подразумевает передачу функции с триггером в одну из функций обработки событий «on_event()»:
  • Только пользователь „admin“ определяет триггеры.
  • Триггеры хранятся в памяти экземпляра Tarantool’а, а не в базе данных. Поэтому триггеры пропадают, когда экземпляр отключают. Чтобы сохранить их, поместите определения функции и настройки триггера в скрипт инициализации Tarantool’а.
  • Триггеры не тратят много ресурсов. Если триггер не задан, то требуется минимум вычислений — разыменование и проверка указателя. Если триггер определен, то стоимость вызова равна стоимости вызова функции.
  • Для одного события можно определить несколько триггеров. В таком случае триггеры выполняются в обратном порядке относительно того, как их определили.
  • Триггеры должны работать в контексте события. Однако результат не определен, если функция содержит запросы, которые при нормальных условиях не могут быть выполнены непосредственно после события, а только после возврата из события. Например, если указать os.exit() или box.rollback() в триггерной функции, запросы не будут выполняться в контексте события.
  • Триггеры можно заменять. Запрос на «замену триггера» подразумевает передачу новой триггерной функции и старой триггерной функции в одну из функций обработки событий «on_event()».
  • Во всех функциях обработки событий «on_event()» есть параметры, которые представляют собой указатели функции, и все они возвращают указатели функции. Следует запомнить, что определение Lua-функции, например, «function f() x = x + 1 end» совпадает с «f = function () x = x + 1 end» – в обоих случаях f получит указатель функции. А «trigger = box.session.on_connect(f)» – это то же самое, что «trigger = box.session.on_connect(function () x = x + 1 end)» – в обоих случаях trigger получит переданный указатель функции.
  • Если вызвать любую из «on_event()» функций без аргументов, то она вернет список соответствующих триггеров. Например, box.session.on_connect() вернет таблицу со всеми connect-trigger функциями.

Пример

Здесь мы записываем события подключения и отключения в журнал на сервере Tarantool’а.

log = require('log')

function on_connect_impl()
  log.info("connected "..box.session.peer()..", sid "..box.session.id())
end

function on_disconnect_impl()
  log.info("disconnected, sid "..box.session.id())
end

function on_auth_impl(user)
  log.info("authenticated sid "..box.session.id().." as "..user)
end

function on_connect() pcall(on_connect_impl) end
function on_disconnect() pcall(on_disconnect_impl) end
function on_auth(user) pcall(on_auth_impl, user) end

box.session.on_connect(on_connect)
box.session.on_disconnect(on_disconnect)
box.session.on_auth(on_auth)

Ограничения

Количество частей в индексе

Для TREE-индексов или HASH-индексов максимальное количество – 255 частей (box.schema.INDEX_PART_MAX). Для RTREE-индексов максимальное количество – 1, но это поля типа ARRAY (массив) с размерностью до 20. Для BITSET-индексов максимальное количество – 1.

Количество индексов в спейсе

128 (box.schema.INDEX_MAX).

Количество полей в кортеже

Теоретически максимальное количество составляет 2 147 483 647 полей (box.schema.FIELD_MAX). Практически максимальное количество указано в поле field_count спейса или соответствует максимальной длине кортежа.

Количество байтов в кортеже

Максимальное количество байтов в кортеже примерно равно memtx_max_tuple_size или vinyl_max_tuple_size (с ресурсами метаданных около 20 байтов на кортеж, которые добавляются к полезным байтам). Значение memtx_max_tuple_size или vinyl_max_tuple_size по умолчанию составляет 1 048 576. Чтобы его увеличить, укажите большее значение при запуске экземпляра Tarantool’а. Например, box.cfg{memtx_max_tuple_size=2*1048576}.

Количество байтов в индекс-ключе

Если поле в кортеже может содержать миллион байтов, то индекс-ключ может содержать миллион байтов, поэтому максимальное количество определяется такими факторами, как количество байтов в кортеже, а не параметрами индекса.

Количество спейсов

Теоретически максимальное количество составляет 2 147 483 647 (box.schema.SPACE_MAX), но практически максимальное количество – около 65 000.

Количество соединений

Практически пределом является количество файловых дескрипторов, которые можно определить с операционной системой.

Размер спейса

Итоговый максимальный размер всех спейсов фактически определяется в memtx_memory, который в свою очередь ограничен общим размером свободной памяти.

Число операций обновления

Максимальное количество операций, возможное в рамках одного обновления (для одного тапла), составляет 4000 (BOX_UPDATE_OP_CNT_MAX).

Количество пользователей и ролей

32 (BOX_USER_MAX).

Длина имени индекса, имени спейса или имени пользователя

65000 (box.schema.NAME_MAX).

Количество реплик в наборе реплик

32 (vclock.VCLOCK_MAX).

Движки базы данных

Движок базы данных представляет собой набор очень низкоуровневых процессов, которые фактически хранят и получают значения в кортежах. Tarantool предлагает два движка базы данных на выбор:

  • memtx (in-memory движок базы данных) используется по умолчанию, который был первым.

  • vinyl (движок для хранения данных на диске) – это рабочий движок на основе пар ключ-значение, который особенно понравится пользователям, предпочитающим записывать данные напрямую на диск, чтобы сократить время восстановления и увеличить размер базы данных.

    С другой стороны, vinyl’у не хватает некоторых функций и параметров, доступных в memtx’е. В соответствующих случаях дается дополнительное описание в виде примечания, которое начинается со слов Примечание про движок базы данных.

Далее в разделе рассмотрим подробнее метод хранения данных с помощью движка базы данных vinyl.

Чтобы указать, что следует использовать именно vinyl, необходимо при создании спейса добавить оператор engine = 'vinyl', например:

space = box.schema.space.create('name', {engine='vinyl'})

Различия между движками memtx и vinyl

Основным различием между движками memtx и vinyl является то, что memtx представляет собой «in-memory» движок, тогда как vinyl – это «дисковый» движок. Как правило, in-memory движок быстрее (каждый запрос обычно выполняется меньше, чем за 1 мс), и движок memtx по праву используется в Tarantool’е по умолчанию, но если база данных больше объема доступной памяти, а добавление дополнительной памяти не представляется возможным, рекомендуется использовать дисковый движок как vinyl.

Характеристика memtx vinyl
Поддерживаемый тип индекса TREE, HASH, RTREE или BITSET TREE
Временные спейсы Поддерживается Не поддерживается
функция random() Поддерживается Не поддерживается
функция alter() Поддерживается Поддерживается с версии 1.10.2 (первичный индекс изменять нельзя)
функция len() Возвращает количество кортежей в спейсе Возвращает максимальное примерное количество кортежей в спейсе
функция count() Занимает одинаковые периоды времени Занимает различное количество времени в зависимости от состояния БД
функция delete() Возвращает удаленный кортеж, если есть таковой Всегда возвращает nil
передача управления Не передает управление на запросах выборки, если не происходит коммит транзакции в журнал упреждающей записи (WAL) Передает управление на запросах выборки или аналогичных: get() или pairs()

Хранение данных с помощью vinyl

Tarantool – это транзакционная, персистентная СУБД, которая хранит 100% данных в оперативной памяти. Основными преимущества хранения данных оперативной памяти являются скорость и простота использования: нет необходимости в оптимизации, однако производительность остается стабильно высокой.

Несколько лет назад мы решили расширить продукт посредством реализации классической технологии хранения как в обычных СУБД: в оперативной памяти хранится лишь кэш данных, а основной объем данных находится на диске. Мы решили, что движок хранения можно будет выбирать независимо для каждой таблицы, как это реализовано в MySQL, но при этом с самого начала будет реализована поддержка транзакций.

Первый вопрос, на который нужен был ответ: создавать свой движок или использовать уже существующую библиотеку? Сообщество разработчиков открытого ПО предлагает готовые библиотеки на выбор. Активнее всего развивалась библиотека RocksDB, которая к настоящему времени стала одной из самых популярных. Есть также несколько менее известных библиотек: WiredTiger, ForestDB, NestDB, LMDB.

Тем не менее, изучив исходный код существующих библиотек и взвесив все «за» и «против», мы решили написать свой движок. Одна из причин – все существующие сторонние библиотеки предполагают, что запросы к данным могут поступать из множества потоков операционной системы, и поэтому содержат сложные примитивы синхронизации для управления одновременным доступом к данным. Если бы мы решили встраивать одну из них в Tarantool, то пользователи были бы вынуждены нести издержки многопоточных приложений, не получая ничего взамен. Дело в том, что в основе Tarantool’а лежит архитектура на основе акторов. Обработка транзакций в выделенном потоке позволяет обойтись без лишних блокировок, межпроцессного взаимодействия и других затрат ресурсов, которые забирают до 80% процессорного времени в многопоточных СУБД.

_images/actor_threads.png

Процесс Tarantool’а состоит из заданного количества потоков

Если изначально проектировать движок с учетом кооперативной многозадачности, можно не только существенно ускорить работу, но и реализовать приемы оптимизации, слишком сложные для многопоточных движков. В общем, использование стороннего решения не привело бы к лучшему результату.

Алгоритм

Отказавшись от идеи внедрения существующих библиотек, необходимо было выбрать архитектуру для использования в качестве основы. Есть два альтернативных подхода к хранению данных на диске: старая модель с использованием B-деревьев и их разновидностей и новая – на основе журнально-структурированных деревьев со слиянием, или LSM-деревьев (Log Structured Merge Tree). MySQL, PostgreSQL и Oracle используют B-деревья, а Cassandra, MongoDB и CockroachDB уже используют LSM-деревья.

Считается, что B-деревья более эффективны для чтения, а LSM-деревья – для записи. Тем не менее, с распространением SSD-дисков, у которых в несколько раз выше производительность чтения по сравнению с производительностью записи, преимущества LSM-деревьев стали очевидны в большинстве сценариев.

Прежде чем разбираться с LSM-деревьями в Tarantool’е, посмотрим, как они работают. Для этого разберем устройство обычного B-дерева и связанные с ним проблемы. «B» в слове B-tree означает «Block», то есть это сбалансированное дерево, состоящее из блоков, которые содержат отсортированные списки пар ключ-значение. Вопросы наполнения дерева, балансировки, разбиения и слияния блоков выходят за рамки данной статьи, подробности вы сможете прочитать в Википедии. В итоге мы получаем отсортированный по возрастанию ключа контейнер, минимальный элемент которого хранится в крайнем левом узле, а максимальный – в крайнем правом. Посмотрим, как в B-дереве осуществляется поиск и вставка данных.

_images/classical_b_tree.png

Классическое B-дерево

Если необходимо найти элемент или проверить его наличие, поиск начинается, как обычно, с вершины. Если ключ обнаружен в корневом блоке, поиск заканчивается; в противном случае, переходим в блок с наибольшим меньшим ключом, то есть в самый правый блок, в котором еще есть элементы меньше искомого (элементы на всех уровнях расположены по возрастанию). Если и там элемент не найден, снова переходим на уровень ниже. В конце концов окажемся в одном из листьев и, возможно, обнаружим искомый элемент. Блоки дерева хранятся на диске и читаются в оперативную память по одному, то есть в рамках одного поиска алгоритм считывает logB(N) блоков, где N – это количество элементов в B-дереве. Запись в самом простом случае осуществляется аналогично: алгоритм находит блок, который содержит необходимый элемент, и обновляет (вставляет) его значение.

Чтобы наглядно представить себе эту структуру данных, возьмем B-дерево на 100 000 000 узлов и предположим, что размер блока равен 4096 байтов, а размер элемента – 100 байтов. Таким образом, в каждом блоке можно будет разместить до 40 элементов с учетом накладных расходов, а в B-дереве будет около 2 570 000 блоков, пять уровней, при этом первые четыре займут по 256 МБ, а последний – до 10 ГБ. Очевидно, что на любом современном компьютере все уровни, кроме последнего, успешно попадут в кэш файловой системы, и фактически любая операция чтения будет требовать не более одной операции ввода-вывода.

Ситуация выглядит существенно менее радужно при смене точки зрения. Предположим, что необходимо обновить один элемент дерева. Так как операции с B-деревьями работают через чтение и запись целых блоков, приходится прочитать 1 блок в память, изменить 100 байт из 4096, а затем записать обновленный блок на диск. Таким образом, нам пришлось записать в 40 раз больше, чем реальный объем измененных данных!

Принимая во внимание, что внутренний размер блока в SSD-дисках может быть 64 КБ и больше, и не любое изменение элемента меняет его целиком, объем «паразитной» нагрузки на диск может быть еще выше.

Феномен таких «паразитных» чтений в литературе и блогах, посвященных хранению на диске, называется read amplification (усложнение чтения), а феномен «паразитной» записи – write amplification (усложнение записи).

Коэффициент усложнения, то есть коэффициент умножения, вычисляется как отношение размера фактически прочитанных (или записанных) данных к реально необходимому (или измененному) размеру. В нашем примере с B-деревом коэффициент составит около 40 как для чтения, так и для записи.

Объем «паразитных» операций ввода-вывода при обновлении данных является одной из основных проблем, которую решают LSM-деревья. Рассмотрим, как это работает.

Ключевое отличие LSM-деревьев от классических B-деревьев заключается в том, что LSM-деревья не просто хранят данные (ключи и значения), а также операции с данными: вставки и удаления.

_images/lsm.png


LSM-дерево:

  • Хранит операторы, а не значения:
    • REPLACE
    • DELETE
    • UPSERT
  • Для каждого оператора назначается LSN Обновление файлов происходит только путем присоединения новых записей, сборка мусора проводится после контрольной точки
  • Журнал транзакций при любых изменениях в системе: vylog

Например, элемент для операции вставки, помимо ключа и значения, содержит дополнительный байт с кодом операции – обозначенный выше как REPLACE. Элемент для операции удаления содержит ключ элемента (хранить значение нет необходимости) и соответствующий код операции – DELETE. Также каждый элемент LSM-дерева содержит порядковый номер операции (log sequence number – LSN), то есть значение монотонно возрастающей последовательности, которое уникально идентифицирует каждую операцию. Таким образом, всё дерево упорядочено сначала по возрастанию ключа, а в пределах одного ключа – по убыванию LSN.

_images/lsm_single.png

Один уровень LSM-дерева

Наполнение LSM-дерева

В отличие от B-дерева, которое полностью хранится на диске и может частично кэшироваться в оперативной памяти, в LSM-дереве разделение между памятью и диском явно присутствует с самого начала. При этом проблема сохранности данных, расположенных в энергозависимой памяти, выносится за рамки алгоритма хранения: ее можно решить разными способами, например, журналированием изменений.

Часть дерева, расположенную в оперативной памяти, называют L0 (level zero – уровень ноль). Объем оперативной памяти ограничен, поэтому для L0 отводится фиксированная область. В конфигурации Tarantool’а, например, размер L0 задается с помощью параметра vinyl_memory. В начале, когда LSM-дерево не содержит элементов, операции записываются в L0. Следует отметить, что элементы в дереве упорядочены по возрастанию ключа, а затем по убыванию LSN, так что в случае вставки нового значения по данному ключу легко обнаружить и удалить предыдущее значение. L0 может быть представлен любым контейнером, который сохраняет упорядоченность элементов. Например, для хранения L0 Tarantool использует B+*-дерево. Операции поиска и вставки – это стандартные операции структуры данных, используемой для представления L0, и мы их подробно рассматривать не будем.

Рано или поздно количество элементов в дереве превысит размер L0. Тогда L0 записывается в файл на диске (который называется забегом – «run») и освобождается под новые элементы. Эта операция называется «дамп» (dump).

_images/dumps.png


Все дампы на диске образуют последовательность, упорядоченную по LSN: диапазоны LSN в файлах не пересекаются, а ближе к началу последовательности находятся файлы с более новыми операциями. Представим эти файлы в виде пирамиды, где новые файлы расположены вверху, а старые внизу. По мере появления новых файлов забегов, высота пирамиды растет. При этом более свежие файлы могут содержать операции удаления или замены для существующих ключей. Для удаления старых данных необходимо производиться сборку мусора (этот процесс иногда называется «слияние» – в английском языке «merge» или «compaction»), объединяя нескольких старых файлов в новый. Если при слиянии мы встречаем две версии одного и того же ключа, то достаточно оставить только более новую версию, а если после вставки ключа он был удален, то из результата можно исключить обе операции.

_images/purge.png


Ключевым фактором эффективности LSM-дерева является то, в какой момент и для каких файлов делается слияние. Представим, что LSM-дерево в качестве ключей хранит монотонную последовательность вида 1, 2, 3 …, и операций удаления нет. В этом случае слияние будет бесполезным – все элементы уже отсортированы, дерево не содержит мусор и можно однозначно определить, в каком файле находится каждый ключ. Напротив, если LSM-дерево содержит много операций удаления, слияние позволит освободить место на диске. Но даже если удалений нет, а диапазоны ключей в разных файлах сильно пересекаются, слияние может ускорить поиск, так как сократит число просматриваемых файлов. В этом случае имеет смысл выполнять слияние после каждого дампа. Однако следует отметить, что такое слияние приведет к перезаписи всех данных на диске, поэтому если чтений мало, то лучше делать слияния реже.

Для оптимальной конфигурации под любой из описанных выше сценариев в LSM-дереве все файлы организованы в пирамиду: чем новее операции с данными, тем выше они находятся в пирамиде. При этом в слиянии участвуют два или несколько соседних файлов в пирамиде; по возможности выбираются файлы примерно одинакового размера.

_images/compaction.png


  • Многоуровневое слияние может охватить любое количество уровней
  • Уровень может содержать несколько файлов

Все соседние файлы примерно одинакового размера составляют уровень LSM-дерева на диске. Соотношение размеров файлов на различных уровнях определяет пропорции пирамиды, что позволяет оптимизировать дерево под интенсивные вставки, либо интенсивные чтения.

Предположим, что размер L0 составляет 100 МБ, а соотношение размеров файлов на каждом уровне (параметр vinyl_run_size_ratio) равно 5, и на каждом уровне может быть не более 2 файлов (параметр vinyl_run_count_per_level). После первых трех дампов на диске появятся 3 файла по 100 МБ, эти файлы образуют уровень L1. Так как 3 > 2, запустится слияние файлов в новый файл размером 300 МБ, а старые будут удалены. Спустя еще 2 дампа снова запустится слияние, на этот раз файлов в 100, 100 и 300 МБ, в результате файл размером 500 МБ переместится на уровень L2 (вспомним, что соотношение размеров уровней равно 5), а уровень L1 останется пустым. Пройдут еще 10 дампов, и получим 3 файла по 500 МБ на уровне L2, в результате чего будет создан один файл размером 1500 МБ. Спустя еще 10 дампов произойдет следующее: 2 раза произведем слияние 3 файлов по 100 МБ, а также 2 раза слияние файлов по 100, 100 и 300 МБ, что приведет к созданию двух файлов на уровне L2 по 500 МБ. Поскольку на уровне L2 уже есть три файла, запустится слияние двух файлов по 500 МБ и одного файла в 1500 МБ. Полученный в результате файл в 2500 МБ, в силу своего размера, переедет на уровень L3.

Процесс может продолжаться до бесконечности, а если в потоке операций с LSM-деревом будет много удалений, образовавшийся в результате слияния файл может переместиться не только вниз по пирамиде, но и вверх, так как окажется меньше исходных файлов, использовавшихся при слиянии. Иными словами, принадлежность файла к уровню достаточно отслеживать логически на основе размера файла и минимального и максимального LSN среди всех хранящихся в нем операций.

Управление формой LSM-дерева

Если число файлов для поиска нужно уменьшить, то соотношение размеров файлов на разных уровнях можно увеличить, и, как следствие, уменьшается число уровней. Если, напротив, необходимо снизить затраты ресурсов, вызванные слиянием, то можно уменьшить соотношение размеров уровней: пирамида будет более высокой, а слияние хотя и выполняется чаще, но работает в среднем с файлами меньшего размера, за счет чего суммарно выполняет меньше работы. В целом, «паразитная запись» в LSM-дереве описывается формулой log_{x}(\\frac {N} {L0}) × x или x × \\frac {ln (\\frac {N} {C0})} {ln(x)}, где N – это общий размер всех элементов дерева, L0 – это размер уровня ноль, а x – это соотношение размеров уровней (параметр level_size_ratio). Если \\frac {N} {C0} = 40 (соотношение диск-память), график выглядит примерно вот так:

_images/curve.png


«Паразитное» чтение при этом пропорционально количеству уровней. Стоимость поиска на каждом уровне не превышает стоимости поиска в B-дереве. Возвращаясь к нашему примеру дерева в 100 000 000 элементов: при наличии 256 МБ оперативной памяти и стандартных значений параметров vinyl_run_size_ratio и vinyl_run_count_per_level, получим коэффициент «паразитной» записи равным примерно 13, коэффициент «паразитной» записи может доходить до 150. Разберемся, почему это происходит.

Поиск по диапазону

Если при поиске по одному ключу алгоритм завершается после первого совпадения, то для поиска всех значений в диапазоне (например, всех пользователей с фамилией «Иванов») необходимо просматривать все уровни дерева.

_images/range_search.png

Поиск по диапазону [24,30)

Формирование искомого диапазона при этом происходит так же, как и при слиянии нескольких файлов: из всех источников алгоритм выбирает ключ с максимальным LSN, отбрасывает остальные операции по этому ключу, сдвигает позицию поиска на следующий ключ и повторяет процедуру.

Удаление

Зачем вообще хранить операции удаления? И почему это не приводит к переполнению дерева, например, в сценарии for i=1,10000000 put(i) delete(i) end?

Роль операций удаления при поиске – сообщать об отсутствии искомого значения, а при слиянии – очищать дерево от «мусорных» записей с более старыми LSN.

Пока данные хранятся только в оперативной памяти, нет необходимости хранить операции удаления. Также нет необходимости сохранять операции удаления после слияния, если оно затрагивает в том числе самый нижний уровень дерева – на нем находятся данные самого старого дампа. Действительно, отсутствие значения на последнем уровне означает, что оно отсутствует в дереве.

  • Нельзя производить удаление из файлов, которые обновляются только путем присоединения новых записей
  • Вместо этого на уровень L0 вносятся маркеры удаленных записей (tombstones)
_images/deletion_1.png

Удаление, шаг 1: вставка удаленной записи в L0

_images/deletion_2.png

Удаление, шаг 2: удаленная запись проходит через промежуточные уровни

_images/deletion_3.png

Удаление, шаг 3: при значительном слиянии удаленная запись удаляется из дерева

Если мы знаем, что удаление следует сразу за вставкой уникального значения – а это частый случай при изменении значения во вторичном индексе – то операцию удаления можно отфильтровывать уже при слиянии промежуточных уровней. Эта оптимизация реализована в vinyl’е.

Преимущества LSM-дерева

Помимо снижения «паразитной» записи, подход с периодическими дампами уровня L0 и слиянием уровней L1-Lk имеет ряд преимуществ перед подходом к записи, используемым в B-деревьях:

  • При дампах и слиянии создаются относительно большие файлы: стандартный размер L0 составляет 50-100 MБ, что в тысячи раз превышает размер блока B-дерева.
  • Большой размер позволяет эффективно сжимать данные перед записью. В Tarantool’е сжатие происходит автоматически, что позволяет еще больше уменьшить «паразитную» запись.
  • Издержки фрагментации отсутствуют, потому что в файле элементы следуют друг за другом без пустот/заполнений.
  • Все операции создают новые файлы, а не заменяют старые данные. Это позволяет избавиться от столь ненавистных нам блокировок, при этом несколько операций могут идти параллельно, не приводя к конфликтам. Это также упрощает создание резервных копий и перенос данных на реплику.
  • Хранение старых версий данных позволяет эффективно реализовать поддержку транзакций, используя подход управления параллельным доступом с помощью многоверсионности.
Недостатки LSM-дерева и их устранение

Одним из ключевых преимуществ B-дерева как структуры данных для поиска является предсказуемость: любая операция занимает не более чем log_{B}(N). В классическом LSM-дереве скорость как чтения, так и записи могут может отличаться в лучшем и худшем случае в сотни и тысячи раз. Например, добавление всего лишь одного элемента в L0 может привести к его переполнению, что в свою очередь, может привести к переполнению L1, L2 и т.д. Процесс чтения может обнаружить исходный элемент в L0, а может задействовать все уровни. Чтение в пределах одного уровня также необходимо оптимизировать, чтобы добиться скорости, сравнимой с B-деревом. К счастью, многие недостатки можно скрасить или полностью устранить с помощью вспомогательных алгоритмов и структур данных. Систематизируем эти недостатки и опишем способы борьбы с ними, используемые в Tarantool’е.

Непредсказуемая скорость записи

Вставка данных в LSM-дерево почти всегда задействует исключительно L0. Как избежать простоя, если заполнена область оперативной памяти, отведенная под L0?

Освобождение L0 подразумевает две долгих операции: запись на диск и освобождение памяти. Чтобы избежать простоя во время записи L0 на диск, Tarantool использует упреждающую запись. Допустим, размер L0 составляет 256 MБ. Скорость записи на диск составляет 10 МБ/с. Тогда для записи L0 на диск понадобится 26 секунд. Скорость вставки данных составляет 10 000 запросов в секунду, а размер одного ключа – 100 байтов. На время записи необходимо зарезервировать около 26 MБ доступной оперативной памяти, сократив реальный полезный размер L0 до 230 MБ.

Все эти расчеты Tarantool делает автоматически, постоянно поддерживая скользящее среднее значение нагрузки на СУБД и гистограмму скорости работы диска. Это позволяет максимально эффективно использовать L0 и избежать истечения времени ожидания доступной памяти для операций записи. При резком всплеске нагрузки ожидание все же возможно, поэтому также существует время ожидания операции вставки (параметр vinyl_timeout), значение которого по умолчанию составляет 60 секунд. Сама запись осуществляется в выделенных потоках, число которых (2 по умолчанию) задается в параметре vinyl_write_threads. Используемое по умолчанию значение 2 позволяет выполнять дамп параллельно со слиянием, что также необходимо для предсказуемой работы системы.

Слияния в Tarantool’е всегда выполняются независимо от дампов, в отдельном потоке выполнения. Это возможно благодаря природе LSM-дерева – после записи файлы в дереве никогда не меняются, а слияние лишь создает новый файл.

К задержкам также может приводить ротация L0 и освобождение памяти, записанной на диск: в процессе записи памятью L0 владеют два потока операционной системы – поток обработки транзакций и поток записи. Хотя в L0 во время ротации элементы не добавляются, он может участвовать в поиске. Чтобы избежать блокировок на чтение во время поиска, поток записи не освобождает записанную память, а оставляет эту задачу потоку обработки транзакций. Само освобождение после завершения дампа происходит мгновенно: для этого в L0 используется специализированный механизм распределения, позволяющий освободить всю память за одну операцию.

_images/dump_from_shadow.png
  • упреждающий дамп
  • загрузка

Дамп происходит из так называемого «теневого» L0, не блокируя новые вставки и чтения

Непредсказуемая скорость чтений

Чтение – самая сложная задача для оптимизации в LSM-деревьях. Главным фактором сложности является большое количество уровней: это не только значительно замедляет поиск, но и потенциально значительно увеличивает требования к оперативной памяти при почти любых попытках оптимизации. К счастью, природа LSM-деревьев, где файлы обновляются только путем присоединения новых записей, позволяет решать эти проблемы нестандартными для традиционных структур данных способами.

_images/read_speed.png
  • постраничный индекс
  • фильтры Блума
  • кэш диапазона кортежей
  • многоуровневое слияние
Сжатие и постраничный индекс

Сжатие данных в B-деревьях – это либо сложнейшая в реализации задача, либо больше средство маркетинга, чем действительно полезный инструмент. Сжатие в LSM-деревьях работает следующим образом:

При любом дампе или слиянии мы разбиваем все данные в одном файле на страницы. Размер страницы в байтах задается в параметре vinyl_page_size, который можно менять отдельно для каждого индекса. Страница не обязана занимать строго то количество байт, которое прописано vinyl_page_size – она может быть чуть больше или чуть меньше, в зависимости от хранящихся в ней данных. Благодаря этому страница никогда не содержит пустот.

Для сжатия используется потоковый алгоритм Facebook под названием «zstd». Первый ключ каждой страницы и смещение страницы в файле добавляются в так называемый постраничный индекс (page index) – отдельный файл, который позволяет быстро найти нужную страницу. После дампа или слияния постраничный индекс созданного файла также записывается на диск.

Все файлы типа .index кэшируются в оперативной памяти, что позволяет найти нужную страницу за одно чтение из файла .run (такое расширение имени файла используется в vinyl’е для файлов, полученных в результате дампа или слияния). Поскольку данные в странице отсортированы, после чтения и декомпрессии нужный ключ можно найти с помощью простого бинарного поиска. За чтение и декомпрессию отвечают отдельные потоки, их количество определяется в параметре vinyl_read_threads.

Tarantool использует единый формат файлов: например, формат данных в файле .run ничем не отличается от формата файла .xlog (файл журнала). Это упрощает резервное копирование и восстановление, а также работу внешних инструментов.

Фильтры Блума

Хотя постраничный индекс позволяет уменьшить количество страниц, просматриваемых при поиске в одном файле, он не отменяет необходимости искать на всех уровнях дерева. Есть важный частный случай, когда необходимо проверить отсутствие данных, и тогда просмотр всех уровней неизбежен: вставка в уникальный индекс. Если данные уже существуют, то вставка в уникальный индекс должна завершиться с ошибкой. Единственный способ вернуть ошибку до завершения транзакции в LSM-дереве – произвести поиск перед вставкой. Такого рода чтения в СУБД образуют целый класс, называемый «скрытыми» или «паразитными» чтениями.

Другая операция, приводящая к скрытым чтениям, – обновление значения, по которому построен вторичный индекс. Вторичные ключи представляют собой обычные LSM-деревья, в которых данные хранятся в другом порядке. Чаще всего, чтобы не хранить все данные во всех индексах, значение, соответствующее данному ключу, целиком сохраняется только в первичном индексе (любой индекс, хранящий и ключ, и значение, называется покрывающим или кластерным), а во вторичном индексе сохраняются лишь поля, по которым построен вторичный индекс, и значения полей, участвующих в первичном индексе. Тогда при любом изменении значения, по которому построен вторичный ключ, приходится сначала удалять из вторичного индекса старый ключ, и только потом вставлять новый. Старое значение во время обновления неизвестно – именно его и нужно читать из первичного ключа с точки зрения внутреннего устройства.

Например:

update t1 set city=’Moscow’ where id=1

Чтобы уменьшить количество чтений с диска, особенно для несуществующих значений, практически все LSM-деревья используют вероятностные структуры данных. Tarantool не исключение. Классический фильтр Блума – это набор из нескольких (обычно 3-5) битовых массивов. При записи для каждого ключа вычисляется несколько хеш-функций, и в каждом массиве выставляется бит, соответствующий значению хеша. При хешировании могут возникнуть коллизии, поэтому некоторые биты могут быть проставлены дважды. Интерес представляют биты, которые оказались не проставлены после записи всех ключей. При поиске также вычисляются выбранные хеш-функции. Если хотя бы в одном из битовых массивов бит не стоит, то значение в файле отсутствует. Вероятность срабатывания фильтра Блума определяется теоремой Байеса: каждая хеш-функция представляет собой независимую случайную величину, благодаря чему вероятность того, что во всех битовых массивах одновременно произойдет коллизия, очень мала.

Ключевым преимуществом реализации фильтров Блума в Tarantool’е является простота настройки. Единственный параметр, который можно менять независимо для каждого индекса, называется vinyl_bloom_fpr (FPR в данном случае означает сокращение от «false positive ratio» – коэффициент ложноположительного срабатывания), который по умолчанию равен 0,05, или 5%. На основе этого параметра Tarantool автоматически строит фильтры Блума оптимального размера для поиска как по полному ключу, так и по компонентам ключа. Сами фильтры Блума хранятся вместе с постраничным индексом в файле .index и кэшируются в оперативной памяти.

Кэширование

Многие привыкли считать кэширование панацеей от всех проблем с производительностью: «В любой непонятной ситуации добавляй кэш». В vinyl’е мы смотрим на кэш скорее как на средство снижения общей нагрузки на диск, и, как следствие, получения более предсказуемого времени ответов на запросы, которые не попали в кэш. В vinyl’е реализован уникальный для транзакционных систем вид кэша под названием «кэш диапазона кортежей» (range tuple cache). В отличие от RocksDB, например, или MySQL, этот кэш хранит не страницы, а уже готовые диапазоны значений индекса, после их чтения с диска и слияния всех уровней. Это позволяет использовать кэш для запросов как по одному ключу, так и по диапазону ключей. Поскольку в кэше хранятся только горячие данные, а не, скажем, страницы (в странице может быть востребована лишь часть данных), оперативная память используется наиболее оптимально. Размер кэша задается в параметре vinyl_cache.

Управление сборкой мусора

Возможно, добравшись до этого места вы уже начали терять концентрацию и нуждаетесь в заслуженной дозе допамина. Самое время сделать перерыв, так как для того, чтобы разобраться с оставшейся частью, понадобятся серьезные усилия.

В vinyl’е устройство одного LSM-дерева – это лишь фрагмент мозаики. Vinyl создает и обслуживает несколько LSM-деревьев даже для одной таблицы (так называемого спейса) – по одному дереву на каждый индекс. Но даже один единственный индекс может состоять из десятков LSM-деревьев. Попробуем разобраться, зачем.

Рассмотрим наш стандартный пример: 100 000 000 записей по 100 байтов каждая. Через некоторое время на самом нижнем уровне LSM у нас может оказаться файл размером 10 ГБ. Во время слияния последнего уровня мы создадим временный файл, который также будет занимать около 10 ГБ. Данные на промежуточных уровнях тоже занимают место: по одному и тому же ключу дерево может хранить несколько операций. Суммарно для хранения 10 ГБ полезных данных нам может потребоваться до 30 ГБ свободного места: 10 ГБ на последний уровень, 10 ГБ на временный файл и 10 ГБ на всё остальное. А если данных не 1 ГБ, а 1 ТБ? Требовать, чтобы количество свободного места на диске всегда в несколько раз превышало объем полезных данных, экономически нецелесообразно, да и создание файла в 1ТБ может занимать десятки часов. При любой аварии или перезапуске системы операцию придется начинать заново.

Рассмотрим другую проблему. Представим, что первичный ключ дерева – это монотонная последовательность, например, временной ряд. В этом случае основные вставки будут приходиться на правую часть диапазона ключей. Нет смысла заново производить слияние лишь для того, чтобы дописать в конец и без того огромного файла еще несколько миллионов записей.

А если вставки происходят, в основном, в одну часть диапазона ключей, а чтения – из другой части? Как в этом случае оптимизировать форму дерева? Если оно будет слишком высоким, пострадают чтения, если слишком низким – запись.

Tarantool «факторизует» проблему, создавая не одно, а множество LSM-деревьев для каждого индекса. Примерный размер каждого поддерева можно задать в конфигурационном параметре vinyl_range_size. Такие поддеревья называется диапазонами («range»).

_images/factor_lsm.png


Факторизация больших LSM-деревьев с помощью диапазонов

  • Диапазоны отражают статичную структуру упорядоченных файлов
  • Срезы объединяют упорядоченный файл в диапазон

Изначально, пока в индексе мало элементов, он состоит из одного диапазона. По мере добавления элементов суммарный объем может превысить максимальный размер диапазона. В таком случае выполняется операция под названием «разделение» (split), которая делит дерево на две равные части. Разделение происходит по срединному элементу диапазона ключей, хранящихся в дереве. Например, если изначально дерево хранит полный диапазон -inf… +inf, то после разделения по срединному ключу X получим два поддерева: одно будет хранить все ключи от -inf до X, другое – от X до +inf. Таким образом, при вставке или чтении мы однозначно знаем, к какому поддереву обращаться. Если в дереве были удаления и каждый из соседних диапазонов уменьшился, выполняется обратная операция под названием «объединение» (coalesce). Она объединяет два соседних дерева в одно.

Разделение и объединение не приводят к слиянию, созданию новых файлов и прочим тяжеловесным операциям. LSM-дерево – это лишь набор файлов. В vinyl’е мы реализовали специальный журнал метаданных, позволяющий легко отслеживать, какой файл принадлежит какому поддереву или поддеревьям. Журнал имеет расширение .vylog, по формату он совместим с файлом .xlog. Как и файл .xlog, происходит автоматическая ротация файла при каждой контрольной точке. Чтобы избежать повторного создания файлов при разделении и объединении, мы ввели промежуточную сущность – срез (slice). Это ссылка на файл с указанием диапазона значений ключа, которая хранится исключительно в журнале метаданных. Когда число ссылок на файл становится равным нулю, файл удаляется. А когда необходимо произвести разделение или объединение, Tarantool создает срезы для каждого нового дерева, старые срезы удаляет, и записывает эти операции в журнал метаданных. Буквально, журнал метаданных хранит записи вида <идентификатор дерева, идентификатор среза> или <идентификатор среза, идентификатор файла, мин, макс>.

Таким образом, непосредственно тяжелая работа по разбиению дерева на два поддерева, откладывается до слияния и выполняется автоматически. Огромным преимуществом подхода с разделением всего диапазона ключей на диапазоны является возможность независимо управлять размером L0, а также процессом создания дампов и слиянием для каждого поддерева. В результате эти процессы являются управляемыми и предсказуемыми. Наличие отдельного журнала метаданных также упрощает выполнение таких операций, как усечение и удаление – в vinyl’е они обрабатываются мгновенно, потому что работают исключительно с журналом метаданных, а удаление мусора выполняется в фоне.

Расширенные возможности vinyl’а
Upsert (обновление и вставка)

В предыдущих разделах упоминались лишь две операции, которые хранит LSM-дерево: удаление и замена. Давайте рассмотрим, как представлены все остальные. Вставку можно представить с помощью замены – необходимо лишь предварительно убедиться в отсутствии элемента указанным ключом. Для выполнения обновления необходимо предварительно считывать старое значение из дерева, так что и эту операцию проще записать в дерево как замену – это ускорит будущие чтения по этому ключу. Кроме того, обновление должно вернуть новое значение, так что скрытых чтений никак не избежать.

В B-деревьях скрытые чтения почти ничего не стоят: чтобы обновить блок, его в любом случае необходимо прочитать с диска. Для LSM-деревьев идея создания специальной операции обновления, которая не приводила бы к скрытым чтениям, выглядит очень заманчивой.

Такая операция должна содержать как значение по умолчанию, которое нужно вставить, если данных по ключу еще нет, так и список операций обновления, которые нужно выполнить, если значение существует.

На этапе выполнения транзакции Tarantool лишь сохраняет всю операцию в LSM-дереве, а «выполняет» ее уже только во время слияния.

Операция обновления и вставки:

space:upsert(tuple, {{operator, field, value}, ... })
  • Обновление без чтения или вставка
  • Отложенное выполнение
  • Фоновое сжатие операций обновления и вставки предотвращает накапливание операций

К сожалению, если откладывать выполнение операции на этап слияния, возможностей для обработки ошибок не остается. Поэтому Tarantool стремится максимально проверять операции обновления и вставки upsert перед записью в дерево. Тем не менее, некоторые проверки можно выполнить лишь имея старые данные на руках. Например, если обновление прибавляет число к строке или удаляет несуществующее поле.

Операция с похожей семантикой присутствует во многих продуктах, в том числе в PostgreSQL и MongoDB. Но везде она представляет собой лишь синтаксический сахар, объединяющий обновление и вставку, не избавляя СУБД от необходимости выполнять скрытые чтения. Скорее всего, причиной этого является относительная новизна LSM-деревьев в качестве структур данных для хранения.

Хотя обновление и вставка upsert представляет собой очень важную оптимизацию, и ее реализация стоила нам долгой напряженной работы, следует признать, что ее применимость ограничена. Если в таблице есть вторичные ключи или триггеры, скрытых чтений не избежать. А если у вас есть сценарии, для которых не нужны вторичные ключи и обновление после завершения транзакции однозначно не приведет к ошибкам – эта операция для вас.

Небольшая история, связанная с этим оператором: vinyl только начинал «взрослеть», и мы впервые запустили операцию обновления и вставки upsert на рабочие серверы. Казалось бы, идеальные условия: огромный набор ключей, текущее время в качестве значения, операции обновления либо вставляют ключ, либо обновляют текущее время, редкие операции чтения. Нагрузочные тесты показали отличные результаты.

Тем не менее, после пары дней работы процесс Tarantool’а начал потреблять 100 % CPU, а производительность системы упала практически до нуля.

Начали подробно изучать проблему. Оказалось, что распределение запросов по ключам существенно отличалось от того, что мы видели в тестовом окружении. Оно было… очень неравномерное. Большая часть ключей обновлялась 1-2 раза за сутки, и база для них не была нагружена. Но были ключи гораздо более горячие – десятки тысяч обновлений в сутки. Tarantool прекрасно справлялся с этим потоком обновлений. А вот когда по ключу с десятком тысяч операций обновления и вставки upsert происходило чтение, всё шло под откос. Чтобы вернуть последнее значение, Tarantool’у приходилось каждый раз прочитать и «проиграть» историю из десятков тысяч команд обновления и вставки upsert. На стадии проекта мы надеялись, что это произойдет автоматически во время слияния уровней, но до слияния дело даже не доходило: памяти L0 было предостаточно, и дампы не создавались.

Решили мы проблему добавлением фонового процесса, осуществляющего упреждающие чтения для ключей, по которым накопилось больше нескольких десятков операций обновления и вставки upsert с последующей заменой на прочитанное значение.

Вторичные ключи

Не только для операции обновления остро стоит проблема оптимизации скрытых чтений. Даже операция замены при наличии вторичных ключей вынуждена читать старое значение: его нужно независимо удалить из вторичных индексов, а вставка нового элемента может этого не сделать, оставив в индексе мусор.

_images/secondary.png


Если вторичные индексы не уникальны, то удаление из них «мусора» также можно перенести в фазу слияния, что мы и делаем в Tarantool’е. Природа LSM-дерева, в котором файлы обновляются путем присоединения новых записей, позволила нам реализовать в vinyl’е полноценные сериализуемые транзакции. Запросы только для чтения при этом используют старые версии данных и не блокируют запись. Сам менеджер транзакций пока довольно простой: в традиционной классификации он реализует класс MVTO (multiversion timestamp ordering – упорядочение временных меток на основе многоверсионности), при этом в конфликте побеждает та транзакция, что завершилась первой. Блокировок и свойственных им взаимоблокировок нет. Как ни странно, это скорее недостаток, чем преимущество: при параллельном выполнении можно повысить количество успешных транзакций, задерживая некоторые из них в нужный момент на блокировке. Развитие менеджера транзакций в наших ближайших планах. В текущей версии мы сфокусировались на том, чтобы сделать алгоритм корректным и предсказуемым на 100%. Например, наш менеджер транзакций – один из немногих в NoSQL-среде, поддерживающих так называемые «блокировки разрывов» (gap locks).

Tarantool Cartridge

Cluster management in Tarantool is powered by the Tarantool Cartridge framework.

Here we explain how you can benefit with Tarantool Cartridge, a framework for developing, deploying, and managing applications based on Tarantool.

This documentation contains the following sections:

Tarantool Cartridge

A framework for distributed applications development.

About Tarantool Cartridge

Tarantool Cartridge allows you to easily develop Tarantool-based applications and run them on one or more Tarantool instances organized into a cluster.

This is the recommended alternative to the old-school practices of application development for Tarantool.

As a software development kit (SDK), Tarantool Cartridge provides you with utilities and an application template to help:

  • easily set up a development environment for your applications;
  • plug the necessary Lua modules.

The resulting package can be installed and started on one or multiple servers as one or multiple instantiated services – independent or organized into a cluster.

A Tarantool cluster is a collection of Tarantool instances acting in concert. While a single Tarantool instance can leverage the performance of a single server and is vulnerable to failure, the cluster spans multiple servers, utilizes their cumulative CPU power, and is fault-tolerant.

To fully utilize the capabilities of a Tarantool cluster, you need to develop applications keeping in mind they are to run in a cluster environment.

As a cluster management tool, Tarantool Cartridge provides your cluster-aware applications with the following key benefits:

  • horizontal scalability and load balancing via built-in automatic sharding;
  • asynchronous replication;
  • automatic failover;
  • centralized cluster control via GUI or API;
  • automatic configuration synchronization;
  • instance functionality segregation.

A Tarantool Cartridge cluster can segregate functionality between instances via built-in and custom (user-defined) cluster roles. You can toggle instances on and off on the fly during cluster operation. This allows you to put different types of workloads (e.g., compute- and transaction-intensive ones) on different physical servers with dedicated hardware.

Tarantool Cartridge has an external utility called cartridge-cli which provides you with utilities and an application template to help:

  • easily set up a development environment for your applications;
  • plug the necessary Lua modules;
  • pack the applications in an environment-independent way: together with module binaries and Tarantool executables.

Getting started

Prerequisites

To get a template application that uses Tarantool Cartridge and run it, you need to install several packages:

  • tarantool and tarantool-dev (see these instructions);
  • cartridge-cli (see these instructions)
  • git, gcc, cmake and make.
Create your first application

Long story short, copy-paste this into the console:

cartridge create --name myapp
cd myapp
cartridge build
cartridge start -d
cartridge replicasets setup --bootstrap-vshard

That’s all! Now you can visit http://localhost:8081 and see your application’s Admin Web UI:

https://user-images.githubusercontent.com/32142520/109290877-3d30a800-7839-11eb-8fcf-8b3de1237a3b.png

Contributing

The most essential contribution is your feedback, don’t hesitate to open an issue. If you’d like to propose some changes in code, see the contribution guide.

Developer’s guide

For a quick start, skip the details below and jump right away to the Cartridge getting started guide.

For a deep dive into what you can develop with Tarantool Cartridge, go on with the Cartridge developer’s guide.

Introduction

Короче говоря, чтобы разработать и запустить приложение, вам необходимо выполнить следующие шаги:

  1. Install Tarantool Cartridge and other components of the development environment.
  2. Create a project.
  3. Разработать приложение. Если это приложение с поддержкой кластеров, реализуйте его логику в виде отдельной (пользовательской) кластерной роли, чтобы инициализировать базу данных в кластерной среде.
  4. Развернуть приложение на сервере или серверах. Это включает в себя настройку и запуск экземпляров.
  5. Если это приложение с поддержкой кластеров, развернуть кластер.

В следующих разделах подробно описывается каждый из этих шагов.

Установка Tarantool Cartridge

  1. Install cartridge-cli, a command-line tool for developing, deploying, and managing Tarantool applications.
  2. Install git, a version control system.
  3. Install npm, a package manager for node.js.
  4. Install the unzip utility.

Creating a project

To set up your development environment, create a project using the Tarantool Cartridge project template. In any directory, say:

$ cartridge create --name <app_name> /path/to/

This will automatically set up a Git repository in a new /path/to/<app_name>/ directory, tag it with version 0.1.0, and put the necessary files into it.

В этом Git-репозитории можно разработать приложение (просто редактируя файлы из шаблона), подключить необходимые модули, а затем с легкостью упаковать все для развертывания на своих серверах.

The project template creates the <app_name>/ directory with the following contents:

  • файл <имя_приложения>-scm-1.rockspec, где можно указать зависимости приложения.
  • скрипт deps.sh, который решает проблемы с зависимостями из файла .rockspec.
  • файл init.lua, который является точкой входа в ваше приложение.
  • файл .git, необходимый для Git-репозитория.
  • файл .gitignore, чтобы не учитывать ненужные файлы.
  • файл env.lua, который устанавливает общие пути для модулей, чтобы приложение можно было запустить из любой директории.
  • файл custom-role.lua, который представляет собой объект-заполнитель для пользовательской кластерной роли.

The entry point file (init.lua), among other things, loads the cartridge module and calls its initialization function:

...
local cartridge = require('cartridge')
...
cartridge.cfg({
-- cartridge options example
  workdir = '/var/lib/tarantool/app',
  advertise_uri = 'localhost:3301',
  cluster_cookie = 'super-cluster-cookie',
  ...
}, {
-- box options example
  memtx_memory = 1000000000,
  ... })
 ...

Вызов cartridge.cfg() позволяет управлять экземпляром через административную консоль, но не вызывает box.cfg() для настройки экземпляров.

Предупреждение

Запрещается вызывать функцию box.cfg().

Сам кластер сделает это за вас, когда придет время:

  • загрузить текущий экземпляр, когда вы:
    • выполните cartridge.bootstrap() в административной консоли, или
    • нажмете Create (Создать) в веб-интерфейсе;
  • присоединить экземпляр к существующему кластеру, когда вы:
    • выполните cartridge.join_server({uri = ''uri_другого_экземпляра'}) в консоли, или
    • нажмете Join (Присоединить – к уже существующему набору реплик) или Create (Создать – для нового набора реплик) в веб-интерфейсе.

Обратите внимание, что вы можете указать cookie для кластера (параметр cluster_cookie), если необходимо запустить несколько кластеров в одной сети. Cookie может представлять собой любое строковое значение.

Now you can develop an application that will run on a single or multiple independent Tarantool instances (e.g. acting as a proxy to third-party databases) – or will run in a cluster.

If you plan to develop a cluster-aware application, first familiarize yourself with the notion of cluster roles.

Кластерные роли

Cluster roles are Lua modules that implement some specific functions and/or logic. In other words, a Tarantool Cartridge cluster segregates instance functionality in a role-based way.

Since all instances running cluster applications use the same source code and are aware of all the defined roles (and plugged modules), you can dynamically enable and disable multiple different roles without restarts, even during cluster operation.

Note that every instance in a replica set performs the same roles and you cannot enable/disable roles individually on some instances. In other words, configuration of enabled roles is set up per replica set. See a step-by-step configuration example in this guide.

Встроенные роли

В модуль cartridge входят две встроенные роли, которые реализуют автоматический шардинг:

  • vshard-router обрабатывает ресурсоемкие вычисления в vshard: направляет запросы к узлам хранения данных.

  • vshard-storage работает с большим количеством транзакций в vshard: хранит подмножество набора данных и управляет им.

    Примечание

    For more information on sharding, see the vshard module documentation.

With the built-in and custom roles, you can develop applications with separated compute and transaction handling – and enable relevant workload-specific roles on different instances running on physical servers with workload-dedicated hardware.

Пользовательские роли

You can implement custom roles for any purposes, for example:

  • определять хранимые процедуры;
  • implement extra features on top of vshard;
  • полностью обойтись без vshard;
  • внедрить одну или несколько дополнительных служб, таких как средство уведомления по электронной почте, репликатор и т.д.

To implement a custom cluster role, do the following:

  1. Take the app/roles/custom.lua file in your project as a sample. Rename this file as you wish, e.g. app/roles/custom-role.lua, and implement the role’s logic. For example:

    -- Implement a custom role in app/roles/custom-role.lua
    #!/usr/bin/env tarantool
    local role_name = 'custom-role'
    
    local function init()
    ...
    end
    
    local function stop()
    ...
    end
    
    return {
        role_name = role_name,
        init = init,
        stop = stop,
    }
    

    Here the role_name value may differ from the module name passed to the cartridge.cfg() function. If the role_name variable is not specified, the module name is the default value.

    Примечание

    Имена ролей должны быть уникальными, поскольку невозможно зарегистрировать несколько ролей с одним именем.

  2. Зарегистрируйте новую роль в кластере, изменив вызов cartridge.cfg() в файле входа в приложение init.lua:

    -- Register a custom role in init.lua
    ...
    local cartridge = require('cartridge')
    ...
    cartridge.cfg({
      workdir = ...,
      advertise_uri = ...,
      roles = {'custom-role'},
    })
    ...
    

    где custom-role (пользовательская роль) – это название загружаемого Lua-модуля.

The role module does not have required functions, but the cluster may execute the following ones during the role’s life cycle:

  • init() – это функция инициализации роли.

    Inside the function’s body you can call any box functions: create spaces, indexes, grant permissions, etc. Here is what the initialization function may look like:

    local function init(opts)
        -- The cluster passes an 'opts' Lua table containing an 'is_master' flag.
        if opts.is_master then
            local customer = box.schema.space.create('customer',
                { if_not_exists = true }
            )
            customer:format({
                {'customer_id', 'unsigned'},
                {'bucket_id', 'unsigned'},
                {'name', 'string'},
            })
            customer:create_index('customer_id', {
                parts = {'customer_id'},
                if_not_exists = true,
            })
        end
    end
    

    Примечание

    • Neither vshard-router nor vshard-storage manage spaces, indexes, or formats. You should do it within a custom role: add a box.schema.space.create() call to your first cluster role, as shown in the example above.
    • Тело функции заключено в условный оператор, который позволяет вызывать функции box только на мастерах. Это предотвращает конфликты репликации, так как данные автоматически передаются на реплики.
  • stop() is the role’s termination function. Implement it if initialization starts a fiber that has to be stopped or does any job that needs to be undone on termination.

  • validate_config() and apply_config() are functions that validate and apply the role’s configuration. Implement them if some configuration data needs to be stored cluster-wide.

Next, get a grip on the role’s life cycle to implement the functions you need.

Определение зависимостей для ролей

Можно заставить кластер применить некоторые другие роли, если включена пользовательская роль.

Например:

-- Role dependencies defined in app/roles/custom-role.lua
local role_name = 'custom-role'
...
return {
    role_name = role_name,
    dependencies = {'cartridge.roles.vshard-router'},
    ...
}

Здесь роль vshard-router будет инициализирована автоматически для каждого экземпляра, в котором включена роль custom-role.

Использование нескольких групп vshard storage

Для наборов реплик с ролью vshard-storage можно задавать группы. Например, группы hot и cold предназначены для независимой обработки горячих и холодных данных.

Группы указаны в конфигурации кластера:

-- Specify groups in init.lua
cartridge.cfg({
    vshard_groups = {'hot', 'cold'},
    ...
})

Если ни одна группа не указана, кластер предполагает, что все наборы реплик входят в группу default (по умолчанию).

Если включены несколько групп, каждый набор реплик с включенной ролью vshard-storage должен быть назначен в определенную группу. Эту настройку нельзя изменить впоследствии.

Есть еще одно ограничение – нельзя добавлять группы динамически (такая возможность появится в будущих версиях).

Finally, mind the syntax for router access. Every instance with a vshard-router role enabled initializes multiple routers. All of them are accessible through the role:

local router_role = cartridge.service_get('vshard-router')
router_role.get('hot'):call(...)

If you have no roles specified, you can access a static router as before (when Tarantool Cartridge was unaware of groups):

local vhsard = require('vshard')
vshard.router.call(...)

However, when using the current group-aware API, you must call a static router with a colon:

local router_role = cartridge.service_get('vshard-router')
local default_router = router_role.get() -- or router_role.get('default')
default_router:call(...)
Role’s life cycle (and the order of function execution)

The cluster displays the names of all custom roles along with the built-in vshard-* roles in the web interface. Cluster administrators can enable and disable them for particular instances – either via the web interface or via the cluster public API. For example:

cartridge.admin.edit_replicaset('replicaset-uuid', {roles = {'vshard-router', 'custom-role'}})

If you enable multiple roles on an instance at the same time, the cluster first initializes the built-in roles (if any) and then the custom ones (if any) in the order the latter were listed in cartridge.cfg().

If a custom role has dependent roles, the dependencies are registered and validated first, prior to the role itself.

Кластер вызывает функции роли в следующих случаях:

  • Функция init() обычно выполняется один раз: либо когда администратор включает роль, либо при перезапуске экземпляра. Как правило, достаточно один раз включить роль.
  • Функция stop() – только когда администратор отключает роль, а не во время завершения работы экземпляра.
  • Функция validate_config(): сначала до автоматического вызова box.cfg()` (инициализация базы данных), а затем при каждом обновлении конфигурации.
  • Функция apply_config() – при каждом обновлении конфигурации.

As a tryout, let’s task the cluster with some actions and see the order of executing the role’s functions:

  • Присоединение экземпляра или создание набора реплик (в обоих случаях с включенной ролью):
    1. validate_config()
    2. init()
    3. apply_config()
  • Перезапуск экземпляра с включенной ролью:
    1. validate_config()
    2. init()
    3. apply_config()
  • Отключение роли: stop().
  • При вызове cartridge.confapplier.patch_clusterwide():
    1. validate_config()
    2. apply_config()
  • При запущенном восстановлении после отказа:
    1. validate_config()
    2. apply_config()

Учитывая вышеописанное поведение:

  • Функция init() может:
    • Вызывать функции box.
    • Запускать файбер, и в таком случае функция stop() должна позаботиться о завершении работы файбера.
    • Настраивать встроенный HTTP-сервер.
    • Выполнять любой код, связанный с инициализацией роли.
  • The stop() functions must undo any job that needs to be undone on role’s termination.
  • Функция validate_config() должна валидировать любые изменения конфигурации.
  • Функция apply_config() может выполнять любой код, связанный с изменением конфигурации, например, следить за файбером expirationd.

The validation and application functions together allow you to change the cluster-wide configuration as described in the next section.

Конфигурация пользовательских ролей

Доступны следующие операции:

  • Хранить настройки пользовательских ролей в виде разделов в конфигурации на уровне кластера, например:

    # in YAML configuration file
    my_role:
      notify_url: "https://localhost:8080"
    
    -- in init.lua file
    local notify_url = 'http://localhost'
    function my_role.apply_config(conf, opts)
      local conf = conf['my_role'] or {}
      notify_url = conf.notify_url or 'default'
    end
    
  • Download and upload cluster-wide configuration using the web interface or API (via GET/PUT queries to admin/config endpoint like curl localhost:8081/admin/config and curl -X PUT -d "{'my_parameter': 'value'}" localhost:8081/admin/config).

  • Utilize it in your role’s apply_config() function.

Каждый экземпляр в кластере хранит копию конфигурационного файла в своей рабочей директории (которую можно задать с помощью cartridge.cfg({workdir = ...})):

  • /var/lib/tarantool/<instance_name>/config.yml для экземпляров, развернутых из RPM-пакетов, под управлением systemd.
  • /home/<username>/tarantool_state/var/lib/tarantool/config.yml for instances deployed from tar+gz archives.

The cluster’s configuration is a Lua table, downloaded and uploaded as YAML. If some application-specific configuration data, e.g. a database schema as defined by DDL (data definition language), needs to be stored on every instance in the cluster, you can implement your own API by adding a custom section to the table. The cluster will help you spread it safely across all instances.

Such section goes in the same file with topology-specific and vshard-specific sections that the cluster generates automatically. Unlike the generated, the custom section’s modification, validation, and application logic has to be defined.

Самый распространенный способ заключается в том, чтобы:

  • validate_config(conf_new, conf_old) для валидации изменений, сделанных в новой конфигурации (conf_new) по отношению к старой конфигурации (conf_old).
  • apply_config(conf, opts) для выполнения любого кода, связанного с изменениями конфигурации. Входными данными для этой функции будут применяемая конфигурация (conf, которая и есть новая конфигурация, проверенная чуть ранее с помощью validate_config()), а также параметры (аргумент opts включает в себя описываемый ниже логический флаг is_master ).

Важно

Функция validate_config() должна обнаружить все проблемы конфигурации, которые могут привести к ошибкам apply_config(). Для получения дополнительной информации см. следующий раздел.

When implementing validation and application functions that call box ones for some reason, mind the following precautions:

  • Жизненный цикл роли не предполагает, что кластер автоматически вызовет box.cfg() до вызова validate_config().

    If the validation function calls any box functions (e.g., to check a format), make sure the calls are wrapped in a protective conditional statement that checks if box.cfg() has already happened:

    -- Inside the validate_config() function:
    
    if type(box.cfg) == 'table' then
    
        -- Here you can call box functions
    
    end
    
  • Unlike the validation function, apply_config() can call box functions freely as the cluster applies custom configuration after the automatic box.cfg() call.

    However, creating spaces, users, etc., can cause replication collisions when performed on both master and replica instances simultaneously. The appropriate way is to call such box functions on masters only and let the changes propagate to replicas automatically.

    По выполнении apply_config(conf, opts) кластер передает флаг is_master в таблице opts, который можно использовать для заключения функций из box в защитный условный оператор, если они могут вызвать конфликт:

    -- Inside the apply_config() function:
    
    if opts.is_master then
    
        -- Here you can call box functions
    
    end
    
Пример пользовательской конфигурации

Рассмотрим следующий код как часть реализации модуля роли (custom-role.lua):

#!/usr/bin/env tarantool
-- Custom role implementation

local cartridge = require('cartridge')

local role_name = 'custom-role'

-- Modify the config by implementing some setter (an alternative to HTTP PUT)
local function set_secret(secret)
    local custom_role_cfg = cartridge.confapplier.get_deepcopy(role_name) or {}
    custom_role_cfg.secret = secret
    cartridge.confapplier.patch_clusterwide({
        [role_name] = custom_role_cfg,
    })
end
-- Validate
local function validate_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    if custom_role_cfg.secret ~= nil then
        assert(type(custom_role_cfg.secret) == 'string', 'custom-role.secret must be a string')
    end
    return true
end
-- Apply
local function apply_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    local secret = custom_role_cfg.secret or 'default-secret'
    -- Make use of it
end

return {
    role_name = role_name,
    set_secret = set_secret,
    validate_config = validate_config,
    apply_config = apply_config,
}

После настройки конфигурации выполните одно из следующих действий:

Применение конфигурации пользовательской роли

With the implementation showed by the example, you can call the set_secret() function to apply the new configuration via the administrative console – or an HTTP endpoint if the role exports one.

Функция set_secret() вызывает cartridge.confapplier.patch_clusterwide(), которая производит двухфазную фиксацию транзакций:

  1. Исправляет активную конфигурацию в памяти: копирует таблицу и заменяет раздел "custom-role" в копии на раздел, который задан функцией set_secret().
  2. The cluster checks if the new configuration can be applied on all instances except disabled and expelled. All instances subject to update must be healthy and alive according to the membership module.
  3. (Фаза подготовки) Кластер передает исправленную конфигурацию. Каждый экземпляр валидирует ее с помощью функции validate_config() каждой зарегистрированной роли. В зависимости от результата валидации:
    • В случае успеха (то есть возврата значения true) экземпляр сохраняет новую конфигурацию во временный файл с именем config.prepare.yml в рабочей директории.
    • (Abort phase) Otherwise, the instance reports an error and all the other instances roll back the update: remove the file they may have already prepared.
  4. (Фаза фиксации) После успешной подготовки всех экземпляров кластер фиксирует изменения. Каждый экземпляр:
    1. Создает жесткую ссылку активной конфигурации.
    2. Atomically replaces the active configuration file with the prepared one. The atomic replacement is indivisible – it can either succeed or fail entirely, never partially.
    3. Вызывает функцию apply_config() каждой зарегистрированной роли.

Если любой из этих шагов не будет выполнен, в веб-интерфейсе появится ошибка рядом с соответствующим экземпляром. Кластер не обрабатывает такие ошибки автоматически, их необходимо исправлять вручную.

Такого рода исправлений можно избежать, если функция validate_config() сможет обнаружить все проблемы конфигурации, которые могут привести к ошибкам в apply_config().

Использование встроенного HTTP-сервера

Кластер запускает экземпляр httpd-сервера во время инициализации (cartridge.cfg()). Можно привязать порт к экземпляру через переменную окружения:

-- Get the port from an environmental variable or the default one:
local http_port = os.getenv('HTTP_PORT') or '8080'

local ok, err = cartridge.cfg({
   ...
   -- Pass the port to the cluster:
   http_port = http_port,
   ...
})

Чтобы использовать httpd-экземпляр, получите к нему доступ и настройте маршруты в рамках функции init() для какой-либо роли (например, для роли, которая предоставляет API через HTTP):

local function init(opts)

...

   -- Get the httpd instance:
   local httpd = cartridge.service_get('httpd')
   if httpd ~= nil then
       -- Configure a route to, for example, metrics:
       httpd:route({
               method = 'GET',
               path = '/metrics',
               public = true,
           },
           function(req)
               return req:render({json = stat.stat()})
           end
       )
   end
end

For more information on using Tarantool’s HTTP server, see its documentation.

Реализация авторизации в веб-интерфейсе

To implement authorization in the web interface of every instance in a Tarantool cluster:

  1. Используйте модуль, к примеру, auth с функцией check_password. Данная функция проверяет учетные данные любого пользователя, который пытается войти в веб-интерфейс.

    Функция check_password принимает имя пользователя и пароль и возвращает результат аутентификации: пройдена или нет.

    -- auth.lua
    
    -- Add a function to check the credentials
    local function check_password(username, password)
    
        -- Check the credentials any way you like
    
        -- Return an authentication success or failure
        if not ok then
            return false
        end
        return true
    end
    ...
    
  2. Передайте имя используемого модуля auth в качестве параметра для cartridge.cfg(), чтобы кластер мог использовать его:

    -- init.lua
    
    local ok, err = cartridge.cfg({
        auth_backend_name = 'auth',
        -- The cluster will automatically call 'require()' on the 'auth' module.
        ...
    })
    

    Это добавит кнопку Log in (Войти) в верхний правый угол в веб-интерфейсе, но все же позволит неавторизованным пользователям взаимодействовать с интерфейсом, что удобно для тестирования.

    Примечание

    Кроме того, для авторизации запросов к API кластера можно использовать базовый заголовок HTTP для авторизации.

  3. Чтобы требовать авторизацию каждого пользователя в веб-интерфейсе даже до начальной загрузки кластера, добавьте следующую строку:

    -- init.lua
    
    local ok, err = cartridge.cfg({
        auth_backend_name = 'auth',
        auth_enabled = true,
        ...
    })
    

    С включенной аутентификацией при использовании модуля auth пользователь не сможет даже загрузить кластер без входа в систему. После успешного входа в систему и начальной загрузки можно включить и отключить аутентификацию для всего кластера в веб-интерфейсе, а параметр auth_enabled игнорируется.

Управление версиями приложения

Tarantool Cartridge understands semantic versioning as described at semver.org. When developing an application, create new Git branches and tag them appropriately. These tags are used to calculate version increments for subsequent packing.

Например, если версия вашего приложения – 1.2.1, пометьте текущую ветку тегом 1.2.1 (с аннотациями или без них).

Чтобы получить значение текущей версии из Git, выполните команду:

$ git describe --long --tags
1.2.1-12-g74864f2

Вывод показывает, что после версии 1.2.1 было 12 коммитов. Если мы соберемся упаковать приложение на данном этапе, его полная версия будет 1.2.1-12, а пакет будет называться <имя_приложения>-1.2.1-12.rpm.

Запрещается использовать не семантические теги. Вы не сможете создать пакет из ветки, если последний тег не будет семантическим.

После упаковки приложения его версия сохраняется в файл VERSION в корневой каталог пакета.

Using .cartridge.ignore files

You can add a .cartridge.ignore file to your application repository to exclude particular files and/or directories from package builds.

For the most part, the logic is similar to that of .gitignore files. The major difference is that in .cartridge.ignore files the order of exceptions relative to the rest of the templates does not matter, while in .gitignore files the order does matter.

.cartridge.ignore entry игнорирует все…
target/ папки (поскольку в конце стоит /) под названием target рекурсивно
target файлы или папки под названием target рекурсивно
/target файлы или папки под названием target в самой верхней директории (поскольку в начале стоит /)
/target/ папки под названием target в самой верхней директории (в начале и в конце стоит /)
*.class файлы или папки, оканчивающиеся на .class, рекурсивно
#comment ничего, это комментарий (первый символ – #)
\#comment файлы или папки под названием #comment (\\ для выделения)
target/logs/ папки под названием logs, которые представляют собой поддиректорию папки под названием target
target/*/logs/ папки под названием logs на два уровня ниже папки под названием target (* не включает /)
target/**/logs/ папки под названием logs где угодно в пределах папки target (** включает /)
*.py[co] файлы или папки, оканчивающиеся на .pyc или .pyo, но не на .py!
*.py[!co] файлы или папки, оканчивающиеся на что угодно, кроме c или o
*.file[0-9] файлы или папки, оканчивающиеся на цифру
*.file[!0-9] файлы или папки, оканчивающиеся на что угодно, кроме цифры
* всё
/* всё в самой верхней директории (поскольку в начале стоит /)
**/*.tar.gz файлы *.tar.gz или папки, которые находятся на один или несколько уровней ниже исходной папки
!file файлы и папки будут проигнорированы, даже если они подходят под другие типы

Failover architecture

An important concept in cluster topology is appointing a leader. Leader is an instance which is responsible for performing key operations. To keep things simple, you can think of a leader as of the only writable master. Every replica set has its own leader, and there’s usually not more than one.

Which instance will become a leader depends on topology settings and failover configuration.

An important topology parameter is the failover priority within a replica set. This is an ordered list of instances. By default, the first instance in the list becomes a leader, but with the failover enabled it may be changed automatically if the first one is malfunctioning.

Instance configuration upon a leader change

When Cartridge configures roles, it takes into account the leadership map (consolidated in the failover.lua module). The leadership map is composed when the instance enters the ConfiguringRoles state for the first time. Later the map is updated according to the failover mode.

Every change in the leadership map is accompanied by instance re-configuration. When the map changes, Cartridge updates the read_only setting and calls the apply_config callback for every role. It also specifies the is_master flag (which actually means is_leader, but hasn’t been renamed yet due to historical reasons).

It’s important to say that we discuss a distributed system where every instance has its own opinion. Even if all opinions coincide, there still may be races between instances, and you (as an application developer) should take them into account when designing roles and their interaction.

Leader appointment rules

The logic behind leader election depends on the failover mode: disabled, eventual, or stateful.

Disabled mode

This is the simplest case. The leader is always the first instance in the failover priority. No automatic switching is performed. When it’s dead, it’s dead.

Eventual failover

In the eventual mode, the leader isn’t elected consistently. Instead, every instance in the cluster thinks that the leader is the first healthy instance in the failover priority list, while instance health is determined according to the membership status (the SWIM protocol).

The member is considered healthy if both are true:

  1. It reports either ConfiguringRoles or RolesConfigured state;
  2. Its SWIM status is either alive or suspect.

A suspect member becomes dead after the failover_timout expires.

Leader election is done as follows. Suppose there are two replica sets in the cluster:

  • a single router «R»,
  • two storages, «S1» and «S2».

Then we can say: all the three instances (R, S1, S2) agree that S1 is the leader.

The SWIM protocol guarantees that eventually all instances will find a common ground, but it’s not guaranteed for every intermediate moment of time. So we may get a conflict.

For example, soon after S1 goes down, R is already informed and thinks that S2 is the leader, but S2 hasn’t received the gossip yet and still thinks he’s not. This is a conflict.

Similarly, when S1 recovers and takes the leadership, S2 may be unaware of that yet. So, both S1 and S2 consider themselves as leaders.

Moreover, SWIM protocol isn’t perfect and still can produce false-negative gossips (announce the instance is dead when it’s not).

Stateful failover

Similarly to the eventual mode, every instance composes its own leadership map, but now the map is fetched from an external state provider (that’s why this failover mode called «stateful»). Nowadays there are two state providers supported – etcd and stateboard (standalone Tarantool instance). State provider serves as a domain-specific key-value storage (simply replicaset_uuid -> leader_uuid) and a locking mechanism.

Changes in the leadership map are obtained from the state provider with the long polling technique.

All decisions are made by the coordinator – the one that holds the lock. The coordinator is implemented as a built-in Cartridge role. There may be many instances with the coordinator role enabled, but only one of them can acquire the lock at the same time. We call this coordinator the «active» one.

The lock is released automatically when the TCP connection is closed, or it may expire if the coordinator becomes unresponsive (in stateboard it’s set by the stateboard’s --lock_delay option, for etcd it’s a part of clusterwide configuration), so the coordinator renews the lock from time to time in order to be considered alive.

The coordinator makes a decision based on the SWIM data, but the decision algorithm is slightly different from that in case of eventual failover:

  • Right after acquiring the lock from the state provider, the coordinator fetches the leadership map.
  • If there is no leader appointed for the replica set, the coordinator appoints the first leader according to the failover priority, regardless of the SWIM status.
  • If a leader becomes dead, the coordinator makes a decision. A new leader is the first healthy instance from the failover priority list. If an old leader recovers, no leader change is made until the current leader down. Changing failover priority doesn’t affect this.
  • Every appointment (self-made or fetched) is immune for a while (controlled by the IMMUNITY_TIMEOUT option).
The case: external provider outage

In this case instances do nothing: the leader remains a leader, read-only instances remain read-only. If any instance restarts during an external state provider outage, it composes an empty leadership map: it doesn’t know who actually is a leader and thinks there is none.

The case: coordinator outage

An active coordinator may be absent in a cluster either because of a failure or due to disabling the role everywhere. Just like in the previous case, instances do nothing about it: they keep fetching the leadership map from the state provider. But it will remain the same until a coordinator appears.

Manual leader promotion

It differs a lot depending on the failover mode.

In the disabled and eventual modes, you can only promote a leader by changing the failover priority (and applying a new clusterwide configuration).

In the stateful mode, the failover priority doesn’t make much sense (except for the first appointment). Instead, you should use the promotion API (the Lua cartridge.failover_promote or the GraphQL mutation {cluster{failover_promote()}}) which pushes manual appointments to the state provider.

The stateful failover mode implies consistent promotion: before becoming writable, each instance performs the wait_lsn operation to sync up with the previous one.

Information about the previous leader (we call it a vclockkeeper) is also stored on the external storage. Even when the old leader is demoted, it remains the vclockkeeper until the new leader successfully awaits and persists its vclock on the external storage.

If replication is stuck and consistent promotion isn’t possible, a user has two options: to revert promotion (to re-promote the old leader) or to force it inconsistently (all kinds of failover_promote API has force_inconsistency flag).

Consistent promotion doesn’t work for replicasets with all_rw flag enabled and for single-instance replicasets. In these two cases an instance doesn’t even try to query vclockkeeper and to perform wait_lsn. But the coordinator still appoints a new leader if the current one dies.

Fencing

Neither eventual nor stateful failover modes don’t protect a replicaset from the presence of multiple leaders when the network is partitioned. But fencing does. It enforces at-most-one leader policy in a replicaset.

Fencing operates as a fiber that occasionally checks connectivity with the state provider and with replicas. Fencing fiber runs on vclockkeepers; it starts right after consistent promotion succeeds. Replicasets which don’t need consistency (single-instance and all_rw) don’t defense, though.

The condition for fencing actuation is the loss of both the state provider quorum and at least one replica. Otherwise, if either state provider is healthy or all replicas are alive, the fencing fiber waits and doesn’t intervene.

When fencing is actuated, it generates a fake appointment locally and sets the leader to nil. Consequently, the instance becomes read-only. Subsequent recovery is only possible when the quorum reestablishes; replica connection isn’t a must for recovery. Recovery is performed according to the rules of consistent switchover unless some other instance has already been promoted to a new leader.

Failover configuration

These are clusterwide parameters:

  • mode: «disabled» / «eventual» / «stateful».
  • state_provider: «tarantool» / «etcd».
  • failover_timeout – time (in seconds) to mark suspect members as dead and trigger failover (default: 20).
  • tarantool_params: {uri = "...", password = "..."}.
  • etcd2_params: {endpoints = {...}, prefix = "/", lock_delay = 10, username = "", password = ""}.
  • fencing_enabled: true / false (default: false).
  • fencing_timeout – time to actuate fencing after the check fails (default: 10).
  • fencing_pause – the period of performing the check (default: 2).

It’s required that failover_timeout > fencing_timeout >= fencing_pause.

GraphQL API

Use your favorite GraphQL client (e.g. Altair) for requests introspection:

  • query {cluster{failover_params{}}},
  • mutation {cluster{failover_params(){}}},
  • mutation {cluster{failover_promote()}}.
Stateboard configuration

Like other Cartridge instances, the stateboard supports cartridge.argprase options:

  • listen
  • workdir
  • password
  • lock_delay

Similarly to other argparse options, they can be passed via command-line arguments or via environment variables, e.g.:

.rocks/bin/stateboard --workdir ./dev/stateboard --listen 4401 --password qwerty
Fine-tuning failover behavior

Besides failover priority and mode, there are some other private options that influence failover operation:

  • LONGPOLL_TIMEOUT (failover) – the long polling timeout (in seconds) to fetch new appointments (default: 30);
  • NETBOX_CALL_TIMEOUT (failover/coordinator) – stateboard client’s connection timeout (in seconds) applied to all communications (default: 1);
  • RECONNECT_PERIOD (coordinator) – time (in seconds) to reconnect to the state provider if it’s unreachable (default: 5);
  • IMMUNITY_TIMEOUT (coordinator) – minimal amount of time (in seconds) to wait before overriding an appointment (default: 15).

Конфигурация экземпляров

Cartridge orchestrates a distributed system of Tarantool instances – a cluster. One of the core concepts is clusterwide configuration. Every instance in a cluster stores a copy of it.

Clusterwide configuration contains options that must be identical on every cluster node, such as the topology of the cluster, failover and vshard configuration, authentication parameters and ACLs, and user-defined configuration.

Clusterwide configuration doesn’t provide instance-specific parameters: ports, workdirs, memory settings, etc.

Configuration basics

Конфигурация экземпляра состоит из двух наборов параметров:

Задать эти параметры можно:

  1. В аргументах в командной строке.
  2. В переменных окружения.
  3. В конфигурационном файле формата YAML.
  4. В файле init.lua.

Вышеуказанный порядок определяет приоритет: аргументы в командной строке замещают переменные окружения и т.д.

Независимо от того, как вы запускаете экземпляры, необходимо задать следующие параметры cartridge.cfg() для каждого экземпляра:

  • advertise_uri – либо <ХОСТ>:<ПОРТ>, либо <ХОСТ>:, либо <ПОРТ>. Используется другими экземплярами для подключения. НЕ указывайте 0.0.0.0 – это должен быть внешний IP-адрес, а не привязка сокета.
  • http_port – порт, который используется, чтобы открывать административный веб-интерфейс и API. По умолчанию: 8081. Чтобы отключить, укажите "http_enabled": False.
  • workdir – директория, где хранятся все данные: файлы снимка, журналы упреждающей записи и конфигурационный файл cartridge. По умолчанию: ..

Если вы запустите экземпляры, используя интерфейс командной строки cartridge или systemctl, сохраните конфигурацию в формате YAML, например:

my_app.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
my_app.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
my_app.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}

С помощью интерфейса командной строки cartridge вы можете передать путь к этому файлу в качестве аргумента командной строки --cfg для команды cartridge start – или же указать путь в конфигурации cartridge./.cartridge.yml или ~/.cartridge.yml):

cfg: cartridge.yml
run_dir: tmp/run
apps_path: /usr/local/share/tarantool

С помощью systemctl сохраните файл в формате YAML в /etc/tarantool/conf.d/ (по умолчанию путь systemd) или в место, указанное в переменной окружения TARANTOOL_CFG.

Если вы запускаете экземпляры с помощью tarantool init.lua, необходимо также передать другие параметры конфигурации в качестве параметров командной строки и переменных окружения, например:

$ tarantool init.lua --alias router --memtx-memory 100 --workdir "~/db/3301" --advertise_uri "localhost:3301" --http_port "8080"
Internal representation of clusterwide configuration

In the file system, clusterwide configuration is represented by a file tree. Inside workdir of any configured instance you can find the following directory:

config/
├── auth.yml
├── topology.yml
└── vshard_groups.yml

This is the clusterwide configuration with three default config sectionsauth, topology, and vshard_groups.

Due to historical reasons clusterwide configuration has two appearances:

  • old-style single-file config.yml with all sections combined, and
  • modern multi-file representation mentioned above.

Before cartridge v2.0 it used to look as follows, and this representation is still used in HTTP API and luatest helpers.

# config.yml
---
auth: {...}
topology: {...}
vshard_groups: {...}
...

Beyond these essential sections, clusterwide configuration may be used for storing some other role-specific data. Clusterwide configuration supports YAML as well as plain text sections. It can also be organized in nested subdirectories.

In Lua it’s represented by the ClusterwideConfig object (a table with metamethods). Refer to the cartridge.clusterwide-config module documentation for more details.

Two-phase commit

Cartridge manages clusterwide configuration to be identical everywhere using the two-phase commit algorithm implemented in the cartridge.twophase module. Changes in clusterwide configuration imply applying it on every instance in the cluster.

Almost every change in cluster parameters triggers a two-phase commit: joining/expelling a server, editing replica set roles, managing users, setting failover and vshard configuration.

Two-phase commit requires all instances to be alive and healthy, otherwise it returns an error.

For more details, please, refer to the cartridge.config_patch_clusterwide API reference.

Managing role-specific data

Beside system sections, clusterwide configuration may be used for storing some other role-specific data. It supports YAML as well as plain text sections. And it can also be organized in nested subdirectories.

Role-specific sections are used by some third-party roles, i.e. sharded-queue and cartridge-extensions.

A user can influence clusterwide configuration in various ways. You can alter configuration using Lua, HTTP or GraphQL API. Also there are luatest helpers available.

HTTP API

It works with old-style single-file representation only. It’s useful when there are only few sections needed.

Example:

cat > config.yml << CONFIG
---
custom_section: {}
...
CONFIG

Upload new config:

curl -v "localhost:8081/admin/config" -X PUT --data-binary @config.yml

Download it:

curl -v "localhost:8081/admin/config" -o config.yml

It’s suitable for role-specific sections only. System sections (topology, auth, vshard_groups, users_acl) can be neither uploaded nor downloaded.

If authorization is enabled, use the curl option --user username:password.

GraphQL API

GraphQL API, by contrast, is only suitable for managing plain-text sections in the modern multi-file appearance. It is mostly used by WebUI, but sometimes it’s also helpful in tests:

g.cluster.main_server:graphql({query = [[
    mutation($sections: [ConfigSectionInput!]) {
        cluster {
            config(sections: $sections) {
                filename
                content
            }
        }
    }]],
    variables = {sections = {
      {
        filename = 'custom_section.yml',
        content = '---\n{}\n...',
      }
    }}
})

Unlike HTTP API, GraphQL affects only the sections mentioned in the query. All the other sections remain unchanged.

Similarly to HTTP API, GraphQL cluster {config} query isn’t suitable for managing system sections.

Lua API

It’s not the most convenient way to configure third-party role, but it may be useful for role development. Please, refer to the corresponding API reference:

  • cartridge.config_patch_clusterwide
  • cartridge.config_get_deepcopy
  • cartridge.config_get_readonly

Example (from sharded-queue, simplified):

function create_tube(tube_name, tube_opts)
    local tubes = cartridge.config_get_deepcopy('tubes') or {}
    tubes[tube_name] = tube_opts or {}

    return cartridge.config_patch_clusterwide({tubes = tubes})
end

local function validate_config(conf)
    local tubes = conf.tubes or {}
    for tube_name, tube_opts in pairs(tubes) do
        -- validate tube_opts
    end
    return true
end

local function apply_config(conf, opts)
    if opts.is_master then
        local tubes = cfg.tubes or {}
        -- create tubes according to the configuration
    end
    return true
end
Luatest helpers

Cartridge test helpers provide methods for configuration management:

  • cartridge.test-helpers.cluster:upload_config,
  • cartridge.test-helpers.cluster:download_config.

Internally they wrap the HTTP API.

Example:

g.before_all(function()
    g.cluster = helpers.Cluster.new(...)
    g.cluster:upload_config({some_section = 'some_value'})
    t.assert_equals(
        g.cluster:download_config(),
        {some_section = 'some_value'}
    )
end)

Развертывание приложения

After you’ve developed your application locally, you can deploy it to a test or production environment.

«Deploy» includes packing the application into a specific distribution format, installing to the target system, and running the application.

Развернуть приложение Tarantool Cartridge можно четырьмя способами:

  • в виде rpm-пакета (для эксплуатационной среды);
  • в виде deb-пакета (для эксплуатационной среды);
  • в виде архива tar+gz (для тестирования или для эксплуатационной среды, если отсутствует доступ уровня root).
  • из исходных файлов (только для локального тестирования).
Развертывание приложения в виде пакета rpm или deb

The choice between DEB and RPM depends on the package manager of the target OS. For example, DEB is native for Debian Linux, and RPM – for CentOS.

  1. Упакуйте файлы приложения в распространяемый пакет:

    $ cartridge pack rpm APP_NAME
    # -- OR --
    $ cartridge pack deb APP_NAME
    

    Будет создан RPM-пакет (например, ./my_app-0.1.0-1.rpm) или же DEB-пакет (например, ./my_app-0.1.0-1.deb).

  2. Загрузите пакет на необходимые серверы с поддержкой systemctl.

  3. Установите:

    $ yum install APP_NAME-VERSION.rpm
    # -- OR --
    $ dpkg -i APP_NAME-VERSION.deb
    
  4. Configure the instance(s). Create a file called /etc/tarantool/conf.d/instances.yml. For example:

    my_app:
      cluster_cookie: secret-cookie
    
    my_app.instance-1:
      http_port: 8081
      advertise_uri: localhost:3301
    
    my_app.instance-2:
      http_port: 8082
      advertise_uri: localhost:3302
    

    See details here.

  5. Запустите экземпляры Tarantool’а с соответствующими службами. Например, это можно сделать, используя systemctl:

    # starts a single instance
    $ systemctl start my_app
    
    # starts multiple instances
    $ systemctl start my_app@router
    $ systemctl start my_app@storage_A
    $ systemctl start my_app@storage_B
    
  6. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

    If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:

    1. In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
    2. In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.
Развертывание архива tar+gz
  1. Упакуйте файлы приложения в распространяемый пакет:

    $ cartridge pack tgz APP_NAME
    

    Будет создан архив tar+gz (например, ./my_app-0.1.0-1.tgz).

  2. Upload the archive to target servers, with tarantool and (optionally) cartridge-cli installed.

  3. Распакуйте архив:

    $ tar -xzvf APP_NAME-VERSION.tgz
    
  4. Configure the instance(s). Create a file called /etc/tarantool/conf.d/instances.yml. For example:

    my_app:
      cluster_cookie: secret-cookie
    
    my_app.instance-1:
      http_port: 8081
      advertise_uri: localhost:3301
    
    my_app.instance-2:
      http_port: 8082
      advertise_uri: localhost:3302
    

    See details here.

  5. Запустите экземпляры Tarantool’а. Это можно сделать, используя:

    • tarantoolctl, например:

      $ tarantool init.lua # starts a single instance
      
    • или cartridge, например:

      # in application directory
      $ cartridge start # starts all instances
      $ cartridge start .router_1 # starts a single instance
      
      # in multi-application environment
      $ cartridge start my_app # starts all instances of my_app
      $ cartridge start my_app.router # starts a single instance
      
  6. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

    If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:

    1. In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
    2. In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.
Развертывание из исходных файлов

Такой метод развертывания предназначен только для локального тестирования.

  1. Вытяните все зависимости в директорию .rocks:

    $ tarantoolctl rocks make

  2. Configure the instance(s). Create a file called /etc/tarantool/conf.d/instances.yml. For example:

    my_app:
      cluster_cookie: secret-cookie
    
    my_app.instance-1:
      http_port: 8081
      advertise_uri: localhost:3301
    
    my_app.instance-2:
      http_port: 8082
      advertise_uri: localhost:3302
    

    See details here.

  3. Запустите экземпляры Tarantool’а. Это можно сделать, используя:

    • tarantoolctl, например:

      $ tarantool init.lua # starts a single instance
      
    • или cartridge, например:

      # in application directory
      cartridge start # starts all instances
      cartridge start .router_1 # starts a single instance
      
      # in multi-application environment
      cartridge start my_app # starts all instances of my_app
      cartridge start my_app.router # starts a single instance
      
  4. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

    If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:

    1. In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
    2. In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.

Запуск/остановка экземпляров

В зависимости от способа развертывания вы можете запускать/останавливать экземпляры, используя tarantool, интерфейс командной строки cartridge или systemctl.

Запуск/остановка с помощью tarantool

С помощью tarantool можно запустить только один экземпляр:

$ tarantool init.lua # the simplest command

Можно также задать дополнительные параметры в командной строке или в переменных окружения.

Чтобы остановить экземпляр, используйте Ctrl+C.

Запуск/остановка с помощью CLI в cartridge

С помощью интерфейса командной строки cartridge, можно запустить один или несколько экземпляров:

$ cartridge start [APP_NAME[.INSTANCE_NAME]] [options]

Возможные параметры:

--script FILE

Точка входа в приложение. По умолчанию:

  • TARANTOOL_SCRIPT, либо
  • ./init.lua, если запуск идет из директории приложения, или же
  • :путь_к_приложениям/:имя_приложения/init.lua в среде с несколькими приложениями.
--apps_path PATH
Путь к директории с приложениями при запуске из среды с несколькими приложениями. По умолчанию: /usr/share/tarantool.
--run_dir DIR
Директория с файлами pid и sock. По умолчанию: TARANTOOL_RUN_DIR или /var/run/tarantool.
--cfg FILE
Cartridge instances YAML configuration file. Defaults to TARANTOOL_CFG or ./instances.yml. The instances.yml file contains cartridge.cfg() parameters described in the configuration section of this guide.
--foreground
Не в фоне.

Например:

cartridge start my_app --cfg demo.yml --run_dir ./tmp/run --foreground

Это запустит все экземпляры Tarantool’а, указанные в файле cfg, не в фоновом режиме с принудительным использованием переменных окружения.

Если ИМЯ_ПРИЛОЖЕНИЯ не указано, cartridge выделит его из имени файла ./*.rockspec.

Если ИМЯ_ЭКЗЕМПЛЯРА не указывается, cartridge прочитает файл cfg и запустит все указанные экземпляры:

# in application directory
cartridge start # starts all instances
cartridge start .router_1 # start single instance

# in multi-application environment
cartridge start my_app # starts all instances of my_app
cartridge start my_app.router # start a single instance

Чтобы остановить экземпляры, выполните команду:

$ cartridge stop [APP_NAME[.INSTANCE_NAME]] [options]

Поддерживаются следующие параметры из команды cartridge start`:

  • --run_dir DIR
  • --cfg FILE
Запуск/остановка с помощью systemctl
  • Чтобы запустить отдельный экземпляр:

    $ systemctl start APP_NAME
    

    This will start a systemd service that will listen to the port specified in instance configuration (http_port parameter).

  • Чтобы запустить несколько экземпляров на одном или нескольких серверах:

    $ systemctl start APP_NAME@INSTANCE_1
    $ systemctl start APP_NAME@INSTANCE_2
    ...
    $ systemctl start APP_NAME@INSTANCE_N
    

    где ИМЯ_ПРИЛОЖЕНИЯ@ЭКЗЕМПЛЯР_N – это имя экземпляра сервиса systemd с инкрементным числом N (уникальным для каждого экземпляра), которое следует добавить к порту 3300 для настройки прослушивания (например, 3301, 3302 и т.д.).

  • Чтобы остановить все сервисы на сервере, используйте команду systemctl stop и укажите имена экземпляров по одному. Например:

    $ systemctl stop APP_NAME@INSTANCE_1 APP_NAME@INSTANCE_2 ... APP_NAME@INSTANCE_<N>
    

When running instances with systemctl, keep these practices in mind:

  • You can specify instance configuration in a YAML file.

    This file can contain these options; see an example here).

    Save this file to /etc/tarantool/conf.d/ (the default systemd path) or to a location set in the TARANTOOL_CFG environment variable (if you’ve edited the application’s systemd unit file). The file name doesn’t matter: it can be instances.yml or anything else you like.

    Here’s what systemd is doing further:

    • obtains app_name (and instance_name, if specified) from the name of the application’s systemd unit file (e.g. APP_NAME@default or APP_NAME@INSTANCE_1);
    • sets default console socket (e.g. /var/run/tarantool/APP_NAME@INSTANCE_1.control), PID file (e.g. /var/run/tarantool/APP_NAME@INSTANCE_1.pid) and workdir (e.g. /var/lib/tarantool/<APP_NAME>.<INSTANCE_NAME>). Environment=TARANTOOL_WORKDIR=${workdir}.%i

    Finally, cartridge looks across all YAML files in /etc/tarantool/conf.d for a section with the appropriate name (e.g. app_name that contains common configuration for all instances, and app_name.instance_1 that contain instance-specific configuration). As a result, Cartridge options workdir, console_sock, and pid_file in the YAML file cartridge.cfg become useless, because systemd overrides them.

  • The default tool for querying logs is journalctl. For example:

    # show log messages for a systemd unit named APP_NAME.INSTANCE_1
    $ journalctl -u APP_NAME.INSTANCE_1
    
    # show only the most recent messages and continuously print new ones
    $ journalctl -f -u APP_NAME.INSTANCE_1
    

    If really needed, you can change logging-related box.cfg options in the YAML configuration file: see log and other related options.

Error handling guidelines

Almost all errors in Cartridge follow the return nil, err style, where err is an error object produced by Tarantool’s errors module. Cartridge doesn’t raise errors except for bugs and functions contracts mismatch. Developing new roles should follow these guidelines as well.

Error objects in Lua

Error classes help to locate the problem’s source. For this purpose, an error object contains its class, stack traceback, and a message.

local errors = require('errors')
local DangerousError = errors.new_class("DangerousError")

local function some_fancy_function()

    local something_bad_happens = true

    if something_bad_happens then
        return nil, DangerousError:new("Oh boy")
    end

    return "success" -- not reachable due to the error
end

print(some_fancy_function())
nil DangerousError: Oh boy
stack traceback:
    test.lua:9: in function 'some_fancy_function'
    test.lua:15: in main chunk

For uniform error handling, errors provides the :pcall API:

local ret, err = DangerousError:pcall(some_fancy_function)
print(ret, err)
nil DangerousError: Oh boy
stack traceback:
    test.lua:9: in function <test.lua:4>
    [C]: in function 'xpcall'
    .rocks/share/tarantool/errors.lua:139: in function 'pcall'
    test.lua:15: in main chunk

`lua print(DangerousError:pcall(error, 'what could possibly go wrong?')) `

nil DangerousError: what could possibly go wrong?
stack traceback:
    [C]: in function 'xpcall'
    .rocks/share/tarantool/errors.lua:139: in function 'pcall'
    test.lua:15: in main chunk

For errors.pcall there is no difference between the return nil, err and error() approaches.

Note that errors.pcall API differs from the vanilla Lua pcall. Instead of true the former returns values returned from the call. If there is an error, it returns nil instead of false, plus an error message.

Remote net.box calls keep no stack trace from the remote. In that case, errors.netbox_eval comes to the rescue. It will find a stack trace from local and remote hosts and restore metatables.

> conn = require('net.box').connect('localhost:3301')
> print( errors.netbox_eval(conn, 'return nil, DoSomethingError:new("oops")') )
nil     DoSomethingError: oops
stack traceback:
        eval:1: in main chunk
during net.box eval on localhost:3301
stack traceback:
        [string "return print( errors.netbox_eval("]:1: in main chunk
        [C]: in function 'pcall'

However, vshard implemented in Tarantool doesn’t utilize the errors module. Instead it uses its own errors. Keep this in mind when working with vshard functions.

Data included in an error object (class name, message, traceback) may be easily converted to string using the tostring() function.

GraphQL

GraphQL implementation in Cartridge wraps the errors module, so a typical error response looks as follows:

{
    "errors":[{
        "message":"what could possibly go wrong?",
        "extensions":{
            "io.tarantool.errors.stack":"stack traceback: ...",
            "io.tarantool.errors.class_name":"DangerousError"
        }
    }]
}

Read more about errors in the GraphQL specification.

If you’re going to implement a GraphQL handler, you can add your own extension like this:

local err = DangerousError:new('I have extension')
err.graphql_extensions = {code = 403}

It will lead to the following response:

{
    "errors":[{
        "message":"I have extension",
        "extensions":{
            "io.tarantool.errors.stack":"stack traceback: ...",
            "io.tarantool.errors.class_name":"DangerousError",
            "code":403
        }
    }]
}
HTTP

In a nutshell, an errors object is a table. This means that it can be swiftly represented in JSON. This approach is used by Cartridge to handle errors via http:

local err = DangerousError:new('Who would have thought?')

local resp = req:render({
    status = 500,
    headers = {
        ['content-type'] = "application/json; charset=utf-8"
    },
    json = json.encode(err),
})
{
    "line":27,
    "class_name":"DangerousError",
    "err":"Who would have thought?",
    "file":".../app/roles/api.lua",
    "stack":"stack traceback:..."
}

Руководство администратора

В данном руководстве рассматривается развертывание и управление кластером Tarantool’а с помощью Tarantool Cartridge.

Примечание

Дополнительную информацию по управлению экземплярами Tarantool’а см. в разделе Администрирование серверной части.

Перед тем, как развертывать кластер, ознакомьтесь с понятием кластерных ролей и разверните экземпляры Tarantool’а в соответствии с предполагаемой топологией кластера.

Развертывание кластера

Чтобы развернуть кластер, сначала настройте все экземпляры Tarantool’а в соответствии с предполагаемой топологией кластера, например:

my_app.router: {"advertise_uri": "localhost:3301", "http_port": 8080, "workdir": "./tmp/router"}
my_app.storage_A_master: {"advertise_uri": "localhost:3302", "http_enabled": False, "workdir": "./tmp/storage-a-master"}
my_app.storage_A_replica: {"advertise_uri": "localhost:3303", "http_enabled": False, "workdir": "./tmp/storage-a-replica"}
my_app.storage_B_master: {"advertise_uri": "localhost:3304", "http_enabled": False, "workdir": "./tmp/storage-b-master"}
my_app.storage_B_replica: {"advertise_uri": "localhost:3305", "http_enabled": False, "workdir": "./tmp/storage-b-replica"}

Затем, запустите экземпляры, например, используя CLI в cartridge:

cartridge start my_app --cfg demo.yml --run_dir ./tmp/run --foreground

And bootstrap the cluster. You can do this via the Web interface which is available at http://<instance_hostname>:<instance_http_port> (in this example, http://localhost:8080).

В веб-интерфейсе выполните следующие действия:

  1. В зависимости от статуса аутентификации:

    • Если аутентификация включена (в эксплуатационной среде), введите свои учетные данные и нажмите Login (Войти):

      _images/auth_creds-border-5px.png

       

    • Если отключен (для удобства тестирования), просто переходите к настройке кластера.

  2. Нажмите Configure (Настроить) рядом с первым ненастроенным сервером, чтобы создать первый набор реплик исключительно для роутера (для обработки ресурсоемких вычислений).

    _images/unconfigured-router-border-5px.png

     

    Во всплывающем окне отметьте флажок роли vshard-router или любой пользовательской роли, для которой роль vshard-router будет зависимой (в данном примере это пользовательская роль под названием app.roles.api).

    (Необязательно) Укажите отображаемое имя для набора реплик, например router.

    _images/create-router-border-5px.png

     

    Примечание

    Как описано в разделе о встроенных ролях, рекомендуется включать кластерные роли в зависимости от рабочей нагрузки на экземпляры, которые работают на физических серверах с аппаратным обеспечением, предназначенным для рабочей нагрузки определенного типа.

    Нажмите Create replica set (Создать набор реплик), и созданный набор реплик отобразится в веб-интерфейсе

    _images/router-replica-set-border-5px.png

     

    Предупреждение

    Обратите внимание: после того, как экземпляр подключится к набору реплик, НЕВОЗМОЖНО это отменить или переподключить его к другому набору реплик.

  3. Создайте новый набор реплик для мастер-узлов хранения данных (для обработки большого количества транзакций).

    Отметьте флажок роли vshard-storage или любой пользовательской роли, для которой роль vshard-storage будет зависимой (в данном примере это пользовательская роль под названием app.roles.storage).

    (Необязательно) Задайте определенную группу, например hot (горячие). Наборы реплик с ролями vshard-storage могут относиться к различным группам. В нашем примере группы hot и cold предназначены для независимой обработки горячих и холодных данных соответственно. Эти группы указаны в конфигурационном файле кластера. По умолчанию, кластер не входит ни в одну группу.

    (Необязательно) Укажите отображаемое имя для набора реплик, например hot-storage.

    Нажмите Create replica set (Создать набор реплик).

    _images/create-storage-border-5px.png

     

  4. (Необязательно) Если этого требует топология, добавьте во второй набор реплик дополнительные хранилища:

    1. Нажмите Configure (Настроить) рядом с другим ненастроенным сервером, который выделен для рабочей нагрузки с большим количеством транзакций.

    2. Нажмите на вкладку Join Replica Set (Присоединиться к набору реплик).

    3. Выберите второй набор реплик и нажмите Join replica set (Присоединиться к набору реплик), чтобы добавить к нему сервер.

      _images/join-storage-border-5px.png

       

  5. В зависимости от топологии кластера:

    • добавьте дополнительные экземпляры к первому или второму набору реплик, или же
    • создайте дополнительные наборы реплик и добавьте в них экземпляры для обработки определенной рабочей нагрузки (вычисления или транзакции).

    Например:

    _images/final-cluster-border-5px.png

     

  6. (Необязательно) По умолчанию все новые наборы реплик vshard-storage получают вес, равный 1, до загрузки vshard в следующем шаге.

    Примечание

    Если вы добавите новый набор реплик после начальной загрузки vshard, как описано в разделе об изменении топологии, он по умолчанию получит вес 0.

    Чтобы разные наборы реплик хранили разное количество сегментов, нажмите Edit (Изменить) рядом с набором реплик, измените значение веса по умолчанию и нажмите Save (Сохранить):

    _images/change-weight-border-5px.png

     

    Для получения дополнительной информации о сегментах и весах набора реплик см. документацию по модулю vshard.

  7. Загрузите vshard, нажав соответствующую кнопку или же выполнив команду cartridge.admin.boostrap_vshard() в административной консоли.

    Эта команда создает виртуальные сегменты и распределяет их по хранилищам.

    С этого момента всю настройку кластера можно выполнять через веб-интерфейс.

Обновление конфигурации

Конфигурация кластера задается в конфигурационном файле формата YAML. Этот файл включает в себя топологию кластера и описания ролей.

У всех экземпляров в кластере Tarantool’а одинаковые настройки. Для этого каждый экземпляр в кластере хранит копию конфигурационного файла, а кластер синхронизирует эти копии: как только вы подтверждаете обновление конфигурации в веб-интерфейсе, кластер валидирует ее (и отклоняет неприемлемые изменения) и передает ее автоматически по всему кластеру.

Чтобы обновить конфигурацию:

  1. Нажмите на вкладку Configuration files (Конфигурационные файлы).

  2. (Необязательно) Нажмите Downloaded (Загруженные), чтобы получить текущую версию конфигурационного файла.

  3. Обновите конфигурационный файл.

    Можно добавлять/изменять/удалять любы разделы, кроме системных: topology, vshard и vshard_groups.

    Чтобы удалить раздел, просто удалите его из конфигурационного файла.

  4. Создайте сжатую копию конфигурационного файла в виде архива в формате .zip и нажмите кнопку Upload configuration (Загрузить конфигурацию), чтобы загрузить ее.

    В нижней части экрана вы увидите сообщение об успешной загрузке конфигурации или ошибку, если новые настройки не были применены.

Управление кластером

В данной главе описывается, как:

  • изменять топологию кластера,
  • включать автоматическое восстановление после отказа,
  • вручную менять мастера в наборе реплик,
  • отключать наборы реплик,
  • исключать экземпляры.
Изменение топологии кластера

При добавлении нового развернутого экземпляра в новый или уже существующий набор реплик:

  1. Кластер валидирует обновление конфигурации, проверяя доступность нового экземпляра с помощью модуля membership.

    Примечание

    Модуль membership работает по протоколу UDP и может производить операции до вызова функции box.cfg.

    Все узлы в кластере должны быть рабочими, чтобы валидация была пройдена.

  2. Новый экземпляр ожидает, пока другой экземпляр в кластере не получит обновление конфигурации (оповещение реализовано с помощью того же модуля membership). На этом шаге у нового экземпляра еще нет своего UUID.

  3. Как только новый экземпляр понимает, что кластер знает о нем, экземпляр вызывает функцию box.cfg и начинает работу.

Оптимальная стратегия подключения новых узлов к кластеру состоит в том, чтобы развертывать новые экземпляры в наборе реплик с нулевым весом для каждого экземпляра, а затем увеличивать вес. Как только вес обновится и все узлы кластера получат уведомление об изменении конфигурации, сегменты начинают мигрировать на новые узлы.

Чтобы добавить в кластер новые узлы, выполните следующие действия:

  1. Разверните новые экземпляры Tarantool, как описано в разделе по развертыванию.

    Если новые узлы не появились в веб-интерфейсе, нажмите Probe server (Найти сервер) и укажите их URI вручную.

    _images/probe-server-border-5px.png

     

    Если узел доступен, он появится в списке.

  2. В веб-интерфейсе:

    • Создайте новый набор реплик с одним из новых экземпляров: нажмите Configure (Настроить) рядом с ненастроенным сервером, отметьте флажками необходимые роли и нажмите Create replica set (Создать набор реплик):

      Примечание

      Если вы добавляете экземпляр vshard-storage, следует помнить, что вес всех таких экземпляров по умолчанию становится равным 0 после начальной загрузки vshard, которая происходит во время первоначального развертывания кластера.

      _images/zero-border-5px.png

       

    • Или добавьте дополнительные экземпляры к существующему набору реплик: нажмите Configure (Настроить) рядом с ненастроенным сервером, нажмите на вкладку Join replica set (Присоединиться к набору реплик), выберите набор реплик и нажмите Join replica set.

    При необходимости повторите действия для других экземпляров, чтобы достичь необходимого уровня резервирования.

  3. При развертывании нового набора реплик vshard-storage заполните необходимую информацию: нажмите Edit (Изменить) рядом с необходимым набором реплик, увеличьте его вес и нажмите Save (Сохранить), чтобы начать балансировку данных.

Вместо веб-интерфейса можно использовать GraphQL для просмотра и изменения топологии кластера. Конечная точка кластера для выполнения запросов GraphQL – /admin/api. Можно пользоваться любыми сторонними клиентами GraphQL, такими как GraphiQL или Altair.

Примеры:

  • вывод списка всех серверов в кластере:

    query {
        servers { alias uri uuid }
    }
    
  • вывод списка всех наборов реплик с серверами:

    query {
        replicasets {
            uuid
            roles
            servers { uri uuid }
        }
    }
    
  • подключение сервера к новому набору реплик с включенной ролью хранилища:

    mutation {
        join_server(
            uri: "localhost:33003"
            roles: ["vshard-storage"]
        )
    }
    
Балансировка данных

Балансировка (решардинг) запускается через определенные промежутки времени и при добавлении в кластер нового набора реплик с весом, отличным от нуля. Для получения дополнительной информации см. раздел по балансировке в документации по модулю vshard.

Самый удобный способ мониторинга процесса балансировки заключается в том, чтобы отслеживать количество активных сегментов на узлах хранения. Сначала в новом наборе реплик 0 активных сегментов. Через некоторое время фоновый процесс балансировки начинает переносить сегменты из других наборов реплик в новый. Балансировка продолжается до тех пор, пока данные не будут распределены равномерно по всем наборам реплик.

Чтобы отслеживать текущее количество сегментов, подключитесь к любому экземпляру Tarantool через административную консоль и выполните команду:

tarantool> vshard.storage.info().bucket
---
- receiving: 0
  active: 1000
  total: 1000
  garbage: 0
  sending: 0
...

Количество сегментов может увеличиваться или уменьшаться в зависимости от того, переносит ли балансировщик сегменты в узел хранения или из него.

Для получения дополнительной информации о параметрах мониторинга см. раздел по мониторингу хранилищ.

Отключение наборов реплик

Под отключением всего набора реплик (например, для технического обслуживания) подразумевается перемещение всех его сегментов в другие наборы реплик.

Чтобы отключить набор реплик, выполните следующие действия:

  1. Нажмите Edit (Изменить) рядом с необходимым набором реплик.

  2. Укажите 0 как значение веса и нажмите Save (Сохранить):

    _images/zero-weight-border-5px.png

     

  3. Подождите, пока процесс балансировки не завершит перенос всех сегментов набора. Можно отслеживать текущее количество сегментов, как описано в разделе по балансировке данных.

Исключение экземпляров

После того, как экземпляр будет исключен из кластера, он никогда не сможет снова участвовать в кластере, поскольку ни один экземпляр не будет принимать его.

Чтобы исключить экземпляр из кластера, нажмите рядом с ним, затем нажмите Expel server (Исключить сервер) и Expel:

_images/expelling-instance-border-5px.png

 

Примечание

Есть два ограничения:

  • Нельзя исключить лидера, если у него есть реплика. Сначала передайте роль лидера.
  • Нельзя исключить vshard-хранилище, если оно хранит сегменты. Установите значение веса на 0 и дождитесь завершения ребалансировки.
Включение автоматического восстановления после отказа

В конфигурации кластера мастер-реплика с включенным автоматическим восстановлением после отказа, если происходит отказ указанного пользователем мастера из любого набора реплик, кластер автоматически выбирает следующую реплику из списка приоритетов и назначает ей роль активного мастера (чтение/запись). Когда вышедший из строя мастер возвращается к работе, его роль восстанавливается, и активный мастер снова становится репликой (только для чтения). Это работает для всех ролей.

Чтобы задать приоритет в наборе реплик:

  1. Нажмите Edit (Изменить) рядом с необходимым набором реплик.

  2. Выполните прокрутку в окне Edit replica set (Изменить набор реплик), чтобы увидеть весь список серверов.

  3. Перенесите реплики на необходимые места в списке приоритета и нажмите Save (Сохранить):

    _images/failover-priority-border-5px.png

     

По умолчанию, автоматическое восстановление после отказа отключено. Чтобы включить его:

  1. Нажмите Failover (Восстановление после отказа):

    _images/failover-border-5px.png

     

  2. В окне Failover control (Управление восстановлением после отказа) нажмите Enable (Включить):

    _images/failover-control-border-5px.png

     

Статус восстановления после отказа изменится на enabled (включено):

_images/enabled-failover-border-5px.png

 

Для получения дополнительной информации см. раздел по репликации.

Смена мастера в наборе реплик

Чтобы вручную сменить мастера в наборе реплик:

  1. Нажмите кнопку Edit (Изменить) рядом с необходимым набором реплик:

    _images/edit-replica-set-border-5px.png

     

  2. Выполните прокрутку в окне Edit replica set (Изменить набор реплик), чтобы увидеть весь список серверов. Мастером будет верхний сервер.

    _images/switch-master-border-5px.png

     

  3. Перенесите необходимый сервер наверх и нажмите Save (Сохранить).

Новый мастер автоматически войдет в режим для чтения и записи, а предыдущий мастер будет использоваться только для чтения. Это работает для всех ролей.

Управление пользователями

На вкладке Users (Пользователи) можно включать и отключать аутентификацию, а также добавлять, удалять, изменять и просматривать пользователей, у которых есть доступ к веб-интерфейсу.

_images/users-tab-border-5px.png

 

Обратите внимание, что вкладка Users (Пользователи) доступна только в том случае, если в веб-интерфейсе реализована авторизация.

Кроме того, некоторые функции (например, удаление пользователей) можно отключить в конфигурации кластера, что регулируется при помощи настройки auth_backend_name, которая передается в cartridge.cfg().

Устранение конфликтов

В Tarantool встроен механизм асинхронной репликации. Как следствие, записи распределяются между репликами с задержкой, поэтому могут возникнуть конфликты.

Для предотвращения конфликтов используется специальный триггер space.before_replace. Он выполняется каждый раз перед внесением изменений в таблицу, для которой он был настроен. Функция триггера реализована на языке программирования Lua. Эта функция принимает в качестве аргументов исходные значения изменяемого кортежа и новые значения. Функция возвращает значение, которое используется для изменения результата операции: это будет новое значение измененного кортежа.

Для операций вставки старое значение отсутствует, поэтому в качестве первого аргумента передается нулевое значение nil.

Для операций удаления отсутствует новое значение, поэтому нулевое значение nil передается в качестве второго аргумента. Функция триггера также может возвращать нулевое значение nil, превращая эту операцию в удаление.

В примере ниже показано, как использовать триггер space.before_replace, чтобы предотвратить конфликты репликации. Предположим, у нас есть таблица box.space.test, которая изменяется в нескольких репликах одновременно. В этой таблице мы храним одно поле полезной нагрузки. Чтобы обеспечить согласованность, мы также сохраняем время последнего изменения в каждом кортеже этой таблицы и устанавливаем триггер space.before_replace, который отдает предпочтение новым кортежам. Ниже приведен код на Lua:

fiber = require('fiber')
-- define a function that will modify the function test_replace(tuple)
        -- add a timestamp to each tuple in the space
        tuple = box.tuple.new(tuple):update{{'!', 2, fiber.time()}}
        box.space.test:replace(tuple)
end
box.cfg{ } -- restore from the local directory
-- set the trigger to avoid conflicts
box.space.test:before_replace(function(old, new)
        if old ~= nil and new ~= nil and new[2] < old[2] then
                return old -- ignore the request
        end
        -- otherwise apply as is
end)
box.cfg{ replication = {...} } -- subscribe

Мониторинг кластера через CLI

В данном разделе описываются параметры, которые можно отслеживать в административной консоли.

Подключение к узлам через CLI

В каждом узле Tarantool (роутер/хранилище) есть административная консоль (интерфейс командной строки) для отладки, мониторинга и разрешения проблем. Консоль выступает в качестве интерпретатора Lua и отображает результат в удобном для восприятия формате YAML. Чтобы подключиться к экземпляру Tarantool через консоль, выполните команду:

$ tarantoolctl connect <instance_hostname>:<port>

где <имя_хоста_экземпляра>:<порт> – это URI данного экземпляра.

Мониторинг хранилищ

Для получения информации об узлах хранения данных используйте vshard.storage.info().

Пример вывода
tarantool> vshard.storage.info()
---
- replicasets:
    <replicaset_2>:
    uuid: <replicaset_2>
    master:
        uri: storage:storage@127.0.0.1:3303
    <replicaset_1>:
    uuid: <replicaset_1>
    master:
        uri: storage:storage@127.0.0.1:3301
  bucket: <!-- buckets status
    receiving: 0 <!-- buckets in the RECEIVING state
    active: 2 <!-- buckets in the ACTIVE state
    garbage: 0 <!-- buckets in the GARBAGE state (are to be deleted)
    total: 2 <!-- total number of buckets
    sending: 0 <!-- buckets in the SENDING state
  status: 1 <!-- the status of the replica set
  replication:
    status: disconnected <!-- the status of the replication
    idle: <idle>
  alerts:
  - ['MASTER_IS_UNREACHABLE', 'Master is unreachable: disconnected']
Список состояний
Код Уровень критичности Описание
0 Зеленый Набор реплик работает в обычном режиме.
1 Желтый Есть некоторые проблемы, но они не влияют на эффективность набора реплик (их стоит отметить, но они не требуют немедленного вмешательства).
2 Оранжевый Набор реплик не восстановился после сбоя.
3 Красный Набор реплик отключен.
Возможные проблемы
  • MISSING_MASTER — В конфигурации набора реплик отсутствует мастер-узел.

    Уровень критичности: Оранжевый.

    Состояние кластера: Ухудшение работы запросов на изменение данных к набору реплик.

    Решение: Задайте мастер-узел для набора реплик, используя API.

  • UNREACHABLE_MASTER — Отсутствует соединение между мастером и репликой.

    Уровень критичности:

    • Если значение бездействия не превышает порог T1 (1 с.) – Желтый,
    • Если значение бездействия не превышает порог T2 (5 с.) – Оранжевый,
    • Если значение бездействия не превышает порог T3 (10 с.) – Красный.

    Состояние кластера: При запросах на чтение из реплики данные могут быть устаревшими по сравнению с данными на мастере.

    Решение: Повторно подключитесь к мастеру: устраните проблемы с сетью, сбросьте текущий мастер, переключитесь на другой мастер.

  • LOW_REDUNDANCY — У мастера есть доступ только к одной реплике.

    Уровень критичности: Желтый.

    Состояние кластера: Коэффициент избыточности хранения данных равен 2. Он ниже минимального рекомендуемого значения для использования в производстве.

    Решение: Проверить конфигурацию кластера:

    • Если в конфигурации указан только один мастер и одна реплика, рекомендуется добавить хотя бы еще одну реплику, чтобы коэффициент избыточности достиг 3.
    • Если в конфигурации указаны три или более реплик, проверьте статусы реплик и сетевое соединение между репликами.
  • INVALID_REBALANCING — Нарушен инвариант балансировки. Во время миграции узел хранения может либо отправлять сегменты, либо получать их. Поэтому не должно быть так, чтобы набор реплик отправлял сегменты в один набор реплик и одновременно получал сегменты из другого набора реплик.

    Уровень критичности: Желтый.

    Состояние кластера: Балансировка приостановлена.

    Решение: Есть две возможные причины нарушения инварианта:

    • Отказ балансировщика.
    • Статус сегмента был изменен вручную.

    В любом случае обратитесь в техническую поддержку Tarantool’а.

  • HIGH_REPLICATION_LAG — Отставание реплики превышает порог T1 (1 с.).

    Уровень критичности:

    • Если отставание не превышает порог T1 (1 с.) – Желтый;
    • Если отставание не превышает порог T2 (5 с.) – Оранжевый.

    Состояние кластера: При запросах только на чтение из реплики данные могут быть устаревшими по сравнению с данными на мастере.

    Решение: Проверьте статус репликации на реплике. Более подробные инструкции приведены в руководстве по разрешению проблем по.

  • OUT_OF_SYNC — Произошла рассинхронизация. Отставание превышает порог T3 (10 с.).

    Уровень критичности: Красный.

    Состояние кластера: При запросах только на чтение из реплики данные могут быть устаревшими по сравнению с данными на мастере.

    Решение: Проверьте статус репликации на реплике. Более подробные инструкции приведены в руководстве по разрешению проблем по.

  • UNREACHABLE_REPLICA — Одна или несколько реплик недоступны.

    Уровень критичности: Желтый.

    Состояние кластера: Коэффициент избыточности хранения данных для данного набора реплик меньше заданного значения. Если реплика стоит следующей в очереди на балансировку (в соответствии с настройками веса), запросы перенаправляются в реплику, которая все еще находится в очереди.

    Решение: Проверьте сообщение об ошибке и выясните, какая реплика недоступна. Если реплика отключена, включите ее. Если это не поможет, проверьте состояние сети.

  • UNREACHABLE_REPLICASET — Все реплики, кроме текущей, недоступны. Уровень критичности: Красный.

    Состояние кластера: Реплика хранит устаревшие данные.

    Решение: Проверьте, включены ли другие реплики. Если все реплики включены, проверьте наличие сетевых проблем на мастере. Если реплики отключены, сначала проверьте их: возможно, мастер работает правильно.

Мониторинг роутеров

Для получения информации о роутерах используйте vshard.router.info().

Пример вывода
tarantool> vshard.router.info()
---
- replicasets:
    <replica set UUID>:
      master:
        status: <available / unreachable / missing>
        uri: <!-- URI of master
        uuid: <!-- UUID of instance
      replica:
        status: <available / unreachable / missing>
        uri: <!-- URI of replica used for slave requests
        uuid: <!-- UUID of instance
      uuid: <!-- UUID of replica set
    <replica set UUID>: ...
    ...
  status: <!-- status of router
  bucket:
    known: <!-- number of buckets with the known destination
    unknown: <!-- number of other buckets
  alerts: [<alert code>, <alert description>], ...
Список состояний
Код Уровень критичности Описание
0 Зеленый Роутер работает в обычном режиме.
1 Желтый Некоторые реплики недоступны, что влияет на скоость выполнения запросов на чтение.
2 Оранжевый Работа запросов на изменение данных ухудшена.
3 Красный Работа запросов на чтение данных ухудшена.
Возможные проблемы

Примечание

В зависимости от характера проблемы используйте либо UUID реплики, либо UUID набора реплик.

  • MISSING_MASTER — В конфигурации одного или нескольких наборов реплик не указан мастер.

    Уровень критичности: Оранжевый.

    Состояние кластера: Частичное ухудшение работы запросов на изменение данных.

    Решение: Укажите мастера в конфигурации.

  • UNREACHABLE_MASTER — Роутер потерял соединение с мастером одного или нескольких наборов реплик.

    Уровень критичности: Оранжевый.

    Состояние кластера: Частичное ухудшение работы запросов на изменение данных.

    Решение: Восстановите соединение с мастером. Сначала проверьте, включен ли мастер. Если он включен, проверьте состояние сети.

  • SUBOPTIMAL_REPLICA — Восстановите соединение с мастером. Сначала проверьте, включен ли мастер. Если он включен, проверьте состояние сети.

    Уровень критичности: Желтый.

    Состояние кластера: Запросы только на чтение направляются на резервную реплику.

    Решение: Проверьте статус оптимальной реплики и ее сетевого подключения.

  • UNREACHABLE_REPLICASET — Набор реплик недоступен как для запросов только на чтение, так и для запросов на изменение данных.

    Уровень критичности: Красный.

    Состояние кластера: Частичное ухудшение работы запросов на изменение данных и на чтение данных.

    Решение: В наборе реплик недоступны мастер и реплика. Проверьте сообщение об ошибке, чтобы найти этот набор реплик. Исправьте ошибку, как описано в решении ошибки UNREACHABLE_REPLICA.

Обновление схемы

Во время перехода на более новую версию Tarantool, пожалуйста, не забудьте:

  1. Остановить кластер
  2. Убедиться, что включена опция upgrade_schema
  3. Затем снова запустить кластер

Это автоматически запустит box.schema.upgrade() на лидере в соответствии с приоритетом (failover priority) в настройках набора реплик.

Аварийное восстановление

См. раздел Аварийное восстановление в руководстве по Tarantool.

Резервное копирование

См. раздел Резервное копирование в руководстве по Tarantool.

Troubleshooting

First of all, see a similar guide in the Tarantool manual. Below you can find other Cartridge-specific problems considered.

Editing clusterwide configuration in WebUI returns an error

Examples:

  • NetboxConnectError: "localhost:3302": Connection refused;
  • Prepare2pcError: Instance state is OperationError, can't apply config in this state.

The root problem: all cluster instances are equal, and all of them store a copy of clusterwide configuration, which must be the same. If an instance degrades (can’t accept new configuration) – the quorum is lost. This prevents further configuration modifications to avoid inconsistency.

But sometimes inconsistency is needed to repair the system, at least partially and temporarily. It can be achieved by disabling degraded instances.

Solution:

  1. Connect to the console of the alive instance.

    tarantoolctl connect unix/:/var/run/tarantool/<app-name>.<instance-name>.control
    
  2. Inspect what’s going on.

    cartridge = require('cartridge')
    report = {}
    for _, srv in pairs(cartridge.admin_get_servers()) do
        report[srv.uuid] = {uri = srv.uri, status = srv.status, message = srv.message}
    end
    return report
    
  3. If you’re ready to proceed, run the following snippet. It’ll disable all instances which are not healthy. After that, you can use the WebUI as usual.

    disable_list = {}
    for uuid, srv in pairs(report) do
        if srv.status ~= 'healthy' then
           table.insert(disable_list, uuid)
        end
    end
    return cartridge.admin_disable_servers(disable_list)
    
  4. When it’s necessary to bring disabled instances back, re-enable them in a similar manner:

    cartridge = require('cartridge')
    enable_list = {}
    for _, srv in pairs(cartridge.admin_get_servers()) do
        if srv.disabled then
           table.insert(enable_list, srv.uuid)
        end
    end
    return cartridge.admin_enable_servers(enable_list)
    

An instance is stuck in the ConnectingFullmesh state upon restart

Example:

_images/stuck-connecting-fullmesh.png

The root problem: after restart, the instance tries to connect to all its replicas and remains in the ConnectingFullmesh state until it succeeds. If it can’t (due to replica URI unavailability or for any other reason) – it’s stuck forever.

Solution:

Set the replication_connect_quorum option to zero. It may be accomplished in two ways:

  • By restarting it with the corresponding option set (in environment variables or in the instance configuration file);

  • Or without restart – by running the following one-liner:

    echo "box.cfg({replication_connect_quorum = 0})" | tarantoolctl connect \
    unix/:/var/run/tarantool/<app-name>.<instance-name>.control
    

I want to run an instance with a new advertise_uri

The root problem: advertise_uri parameter is persisted in the clusterwide configuration. Even if it changes upon restart, the rest of the cluster keeps using the old one, and the cluster may behave in an odd way.

Solution:

The clusterwide configuration should be updated.

  1. Make sure all instances are running and not stuck in the ConnectingFullmesh state (see above).

  2. Make sure all instances have discovered each other (i.e. they look healthy in the WebUI).

  3. Run the following snippet in the Tarantool console. It’ll prepare a patch for the clusterwide configuration.

    cartridge = require('cartridge')
    members = require('membership').members()
    
    edit_list = {}
    changelog = {}
    for _, srv in pairs(cartridge.admin_get_servers()) do
        for _, m in pairs(members) do
            if m.status == 'alive'
            and m.payload.uuid == srv.uuid
            and m.uri ~= srv.uri
            then
                table.insert(edit_list, {uuid = srv.uuid, uri = m.uri})
                table.insert(changelog, string.format('%s -> %s (%s)', srv.uri, m.uri, m.payload.alias))
                break
            end
        end
    end
    return changelog
    

    As a result you’ll see a brief summary like the following one:

    localhost:3301> return changelog
    ---
    - - localhost:13301 -> localhost:3301 (srv-1)
      - localhost:13302 -> localhost:3302 (srv-2)
      - localhost:13303 -> localhost:3303 (srv-3)
      - localhost:13304 -> localhost:3304 (srv-4)
      - localhost:13305 -> localhost:3305 (srv-5)
    ...
    
  4. Finally, apply the patch:

    cartridge.admin_edit_topology({servers = edit_list})
    

The cluster is doomed, I’ve edited the config manually. How do I reload it?

Предупреждение

Please be aware that it’s quite risky and you know what you’re doing. There’s some useful information about clusterwide configuration anatomy and «normal» management API.

But if you’re still determined to reload the configuration manually, you can do (in the Tarantool console):

function reload_clusterwide_config()
    local changelog = {}

    local ClusterwideConfig = require('cartridge.clusterwide-config')
    local confapplier = require('cartridge.confapplier')

    -- load config from filesystem
    table.insert(changelog, 'Loading new config...')

    local cfg, err = ClusterwideConfig.load('./config')
    if err ~= nil then
        return changelog, string.format('Failed to load new config: %s', err)
    end

    -- check instance state
    table.insert(changelog, 'Checking instance config state...')

    local roles_configured_state = 'RolesConfigured'
    local connecting_fullmesh_state = 'ConnectingFullmesh'

    local state = confapplier.wish_state(roles_configured_state, 10)

    if state == connecting_fullmesh_state then
        return changelog, string.format(
            'Failed to reach %s config state. Stuck in %s. ' ..
                'Call "box.cfg({replication_connect_quorum = 0})" in instance console and try again',
            roles_configured_state, state
        )
    end

    if state ~= roles_configured_state then
        return changelog, string.format(
            'Failed to reach %s config state. Stuck in %s',
            roles_configured_state, state
        )
    end

    -- apply config changes
    table.insert(changelog, 'Applying config changes...')

    cfg:lock()
    local ok, err = confapplier.apply_config(cfg)
    if err ~= nil then
        return changelog, string.format('Failed to apply new config: %s', err)
    end

    table.insert(changelog, 'Cluster-wide configuration was successfully updated')

    return changelog
end

reload_clusterwide_config()

This snippet reloads the configuration on a single instance. All other instances continue operating as before.

Примечание

If further configuration modifications are made with a two-phase commit (e.g. via the WebUI or with the Lua API), the active configuration of an active instance will be spread across the cluster.

Repairing cluster using Cartridge CLI repair command

Cartridge CLI has repair command since version 2.3.0.

It can be used to get current topology, remove instance from cluster, change repicaset leader or change instance advertise URI.

Примечание

cartridge repair patches the cluster-wide configuration files of application instances placed ON THE LOCAL MACHINE. It means that running cartridge repair on all machines is user responsibility.

Примечание

It’s not enough to apply new configuration: the configuration should be reloaded by the instance. If your application uses cartridge >= 2.0.0, you can simply use --reload flag to reload configuration. Otherwise, you need to restart instances or reload configuration manually.

Changing instance advertise URI

To change instance advertise URI you have to perform these actions:

  1. Start instance with a new advertise URI. The easiest way is to change advertise_uri value in the instance configuration file).

  2. Make sure instances are running and not stuck in the ConnectingFullmesh state (see above).

  3. Get instance UUID: * open server details tab in WebUI; * call cartridge repair list-topology --name <app-name> and find desired instance UUID: * get instance box.info().uuid:

    echo "return box.info().uuid" | tarantoolctl connect \
    unix/:/var/run/tarantool/<app-name>.<instance-name>.control
    
  4. Now we need to update instance advertise URI in all instances cluster-wide configuration files on each machine. Run cartridge repair set-advertise-uri with --dry-run flag on each machine to check cluster-wide config changes computed by cartridge-cli:

    cartridge repair set-advertise-uri \
      --name myapp \
      --dry-run \
      <instance-uuid> <new-advertise-uri>
    
  5. Run cartridge repair set-advertise-uri without --dry-run flag on each machine to apply config changes computed by cartridge-cli. If your application uses cartridge >= 2.0.0, you can specify --reload flag to load new cluter-wide configuration on instances. Otherwise, you need to restart instances or reload configuration manually.

    cartridge repair set-advertise-uri \
      --name myapp \
      --verbose \
      --reload \
      <instance-uuid> <new-advertise-uri>
    
Changing replicaset leader

You can change replicaset leader using cartridge repair command.

  1. Get replicaset UUID and new leader UUID (in WebUI or by calling cartridge repair list-topology --name <app-name>).

  2. Now we need to update cluster-wide config for all instances on each machine. Run cartridge repair set-leader with --dry-run flag on each machine to check cluster-wide config changes computed by `` cartridge-cli``:

    cartridge repair set-leader \
      --name myapp \
      --dry-run \
      <replicaset-uuid> <instance-uuid>
    
  3. Run cartridge repair set-advertise-uri without --dry-run flag on each machine to apply config changes computed by cartridge-cli. If your application uses cartridge >= 2.0.0, you can specify --reload flag to load new cluter-wide configuration on instances. Otherwise, you need to restart instances or reload configuration manually.

    cartridge repair set-leader \
      --name myapp \
      --verbose \
      --reload \
      <replicaset-uuid> <instance-uuid>
    
Removing instance from the cluster

You can remove instance from cluster using cartridge repair command.

  1. Get instance UUID: * open server details tab in WebUI; * call cartridge repair list-topology --name <app-name> and find desired instance UUID: * get instance box.info().uuid:

    echo "return box.info().uuid" | tarantoolctl connect \
    unix/:/var/run/tarantool/<app-name>.<instance-name>.control
    
  2. Now we need to update cluster-wide config for all instances on each machine. Run cartridge repair remove-instance with --dry-run flag on each machine to check cluster-wide config changes computed by cartridge-cli:

    cartridge repair remove-instance \
      --name myapp \
      --dry-run \
      <replicaset-uuid>
    
  3. Run cartridge repair remove-instance without --dry-run flag on each machine to apply config changes computed by cartridge-cli. If your application uses cartridge >= 2.0.0, you can specify --reload flag to load new cluter-wide configuration on instances. Otherwise, you need to restart instances or reload configuration manually.

    cartridge repair set-leader \
      --name myapp \
      --verbose \
      --reload \
      <replicaset-uuid> <instance-uuid>
    

Table of contents

Module cartridge

Tarantool framework for distributed applications development.

Cartridge provides you a simple way to manage distributed applications operations. The cluster consists of several Tarantool instances acting in concert. Cartridge does not care about how the instances start, it only cares about the configuration of already running processes.

Cartridge automates vshard and replication configuration, simplifies custom configuration and administrative tasks.

Functions
cfg (opts, box_opts)

Initialize the cartridge module.

After this call, you can operate the instance via Tarantool console. Notice that this call does not initialize the database - box.cfg is not called yet. Do not try to call box.cfg yourself: cartridge will do it when it is time.

Both cartridge.cfg and box.cfg options can be configured with command-line arguments or environment variables.

Parameters:

  • opts: Available options are:
    • workdir: (optional string) a directory where all data will be stored: snapshots, wal logs and cartridge config file.(default: «.», overridden byenv TARANTOOL_WORKDIR ,args --workdir )
    • advertise_uri: (optional string) either «<HOST>:<PORT>» or «<HOST>:» or «<PORT>».Used by other instances to connect to the current one.When <HOST> isn’t specified, it’s detected as the only non-local IP address.If there is more than one IP address available - defaults to «localhost».When <PORT> isn’t specified, it’s derived as follows:If the TARANTOOL_INSTANCE_NAME has numeric suffix _<N>, then <PORT> = 3300+<N>.Otherwise default <PORT> = 3301 is used.
    • cluster_cookie: (optional string) secret used to separate unrelated applications, whichprevents them from seeing each other during broadcasts.Also used as admin password in HTTP and binary connections and forencrypting internal communications.Allowed symbols are [a-zA-Z0-9_.~-] .(default: «secret-cluster-cookie», overridden byenv TARANTOOL_CLUSTER_COOKIE ,args --cluster-cookie )
    • swim_broadcast: (optional boolean) Announce own advertise_uri over UDP broadcast.Cartridge health-checks are governed by SWIM protocol. To simplifyinstances discovery on start it can UDP broadcast all networksknown from getifaddrs() C call. The broadcast is sent to severalports: default 3301, the <PORT> from the advertise_uri option,and its neighbours <PORT>+1 and <PORT>-1.(Added in v2.3.0-23,default: true, overridden byenv TARANTOOL_SWIM_BROADCAST,args –swim-broadcast)
    • bucket_count: (optional number) bucket count for vshard cluster. See vshard doc for more details.(default: 30000, overridden byenv TARANTOOL_BUCKET_COUNT ,args --bucket-count )
    • vshard_groups: (optional {[string]=VshardGroup,…}) vshard storage groups, table keys used as names
    • http_enabled: (optional boolean) whether http server should be started(default: true, overridden byenv TARANTOOL_HTTP_ENABLED,args –http-enabled)
    • webui_enabled: (optional boolean) whether WebUI and corresponding API (HTTP + GraphQL) should beinitialized. Ignored if http_enabled is false . Doesn’taffect auth_enabled .(Added in v2.4.0-38,default: true, overridden byenv TARANTOOL_WEBUI_ENABLED ,args --webui-enabled )
    • http_port: (string or number) port to open administrative UI and API on(default: 8081, derived from`TARANTOOL_INSTANCE_NAME`,overridden byenv TARANTOOL_HTTP_PORT,args –http-port)
    • http_host: (optional string) host to open administrative UI and API on(Added in v2.4.0-42default: «0.0.0.0», overridden byenv TARANTOOL_HTTP_HOST ,args --http-host )
    • alias: (optional string) human-readable instance name that will be available in administrative UI(default: argparse instance name, overridden byenv TARANTOOL_ALIAS,args –alias)
    • roles: (table) list of user-defined roles that will be availableto enable on the instance_uuid
    • auth_enabled: (optional boolean) toggle authentication in administrative UI and API(default: false)
    • auth_backend_name: (optional string) user-provided set of callbacks related to authentication
    • console_sock: (optional string) Socket to start console listening on.(default: nil, overridden byenv TARANTOOL_CONSOLE_SOCK ,args --console-sock )
    • webui_blacklist: (optional {string,…}) List of pages to be hidden in WebUI.(Added in v2.0.1-54, default: {} )
    • upgrade_schema: (optional boolean) Run schema upgrade on the leader instance.(Added in v2.0.2-3,default: false , overridden byenv TARANTOOL_UPGRADE_SCHEMA args --upgrade-schema )
    • roles_reload_allowed: (optional boolean) Allow calling cartridge.reload_roles.(Added in v2.3.0-73, default: false )
    • upload_prefix: (optional string) Temporary directory used for saving files during clusterwideconfig upload. If relative path is specified, it’s evaluatedrelative to the workdir .(Added in v2.4.0-43,default: /tmp , overridden byenv TARANTOOL_UPLOAD_PREFIX ,args --upload-prefix )
  • box_opts: (optional table) tarantool extra box.cfg options (e.g. memtx_memory),that may require additional tuning

Returns:

true

Or

(nil)

(table) Error description

reload_roles ()

Perform hot-reload of cartridge roles code.

This is an experimental feature, it’s only allowed if the application enables it explicitly: cartridge.cfg({roles_reload_allowed = true}) .

Reloading starts by stopping all roles and restoring the initial state. It’s supposed that a role cleans up the global state when stopped, but even if it doesn’t, cartridge kills all fibers and removes global variables and HTTP routes.

All Lua modules that were loaded during cartridge.cfg are unloaded, including supplementary modules required by a role. Modules, loaded before cartridge.cfg aren’t affected.

Instance performs roles reload in a dedicated state ReloadingRoles . If reload fails, the instance enters the ReloadError state, which can later be retried. Otherwise, if reload succeeds, instance proceeds to the ConfiguringRoles state and initializes them as usual with validate_config() , init() , and apply_config() callbacks.

Returns:

(boolean) true

Or

(nil)

(table) Error description

is_healthy ()

Check the cluster health. It is healthy if all instances are healthy.

The function is designed mostly for testing purposes.

Returns:

(boolean) true / false

Tables
VshardGroup

Vshard storage group configuration.

Every vshard storage must be assigned to a group.

Fields:

  • bucket_count: (number) Bucket count for the storage group.
Global functions
_G.cartridge_get_schema ()

Get clusterwide DDL schema.

(Added in v1.2.0-28)

Returns:

(string) Schema in YAML format

Or

(nil)

(table) Error description

_G.cartridge_set_schema (schema)

Apply clusterwide DDL schema.

(Added in v1.2.0-28)

Parameters:

  • schema: (string) in YAML format

Returns:

(string) The same new schema

Or

(nil)

(table) Error description

Clusterwide DDL schema
get_schema ()

Get clusterwide DDL schema. It’s like _G.cartridge_get_schema, but isn’t a global variable.

(Added in v2.0.1-54)

Returns:

(string) Schema in YAML format

Or

(nil)

(table) Error description

set_schema (schema)

Apply clusterwide DDL schema. It’s like _G.cartridge_set_schema, but isn’t a global variable.

(Added in v2.0.1-54)

Parameters:

  • schema: (string) in YAML format

Returns:

(string) The same new schema

Or

(nil)

(table) Error description

Cluster administration
ServerInfo

Instance general information.

Fields:

  • alias: (string) Human-readable instance name.
  • uri: (string)
  • uuid: (string)
  • disabled: (boolean)
  • status: (string) Instance health.
  • message: (string) Auxilary health status.
  • replicaset: (ReplicasetInfo) Circular reference to a replicaset.
  • priority: (number) Leadership priority for automatic failover.
  • clock_delta: (number) Difference between remote clock and the current one (inseconds), obtained from the membership module (SWIM protocol).Positive values mean remote clock are ahead of local, and viceversa.
  • zone: (string)
ReplicasetInfo

Replicaset general information.

Fields:

  • uuid: (string) The replicaset UUID.
  • roles: ({string,…}) Roles enabled on the replicaset.
  • status: (string) Replicaset health.
  • master: (ServerInfo) Replicaset leader according to configuration.
  • active_master: (ServerInfo) Active leader.
  • weight: (number) Vshard replicaset weight.Matters only if vshard-storage role is enabled.
  • vshard_group: (string) Name of vshard group the replicaset belongs to.
  • all_rw: (boolean) A flag indicating that all servers in the replicaset should be read-write.
  • alias: (string) Human-readable replicaset name.
  • servers: ({ServerInfo,…}) Circular reference to all instances in the replicaset.
admin_get_servers ([uuid])

Get servers list. Optionally filter out the server with the given uuid.

Parameters:

Returns:

({ServerInfo,…})

Or

(nil)

(