Рейтинг@Mail.ru

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

Замечание

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

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

Общие сведения

Сервер приложений + СУБД

Tarantool представляет собой сервер приложений на языке Lua, интегрированный с СУБД. В основе Tarantool’а лежат файберы (fibers), что означает, что несколько Tarantool-приложений могут работать в одном потоке (thread), при этом каждый экземпляр Tarantool-сервера может одновременно запускать несколько потоков для обработки ввода-вывода данных и фоновых задач. Tarantool включает в себя LuaJIT (Just In Time) - Lua-компилятор, Lua-библиотеки для наиболее распространенных приложений, а также сервер базы данных Tarantool’а, который представляет собой широко признанную СУБД NoSQL. Таким образом, Tarantool используется для всех тех целей, которые принесли популярность node.js и Twisted, и более того - поддерживает персистентность данных.

Tarantool – это open-source проект. Исходный код открыт для всех и распространяется бесплатно согласно лицензии BSD license. Поддерживаемые платформы: GNU / Linux, Mac OS и FreeBSD.

Создателем Tarantool’а – а также его основным пользователем – является компания Mail.Ru, крупнейшая Интернет-компания России (30 млн пользователей, 25 млн электронных писем в день, веб-сайт в списке top 40 международного Alexa-рейтинга). Tarantool используется для обработки самых «горячих» данных Mail.Ru, таких как данные пользовательских онлайн-сессий, настройки онлайн-приложений, кэширование сервисных данных, алгоритмы распределения данных и шардинга, и т.д. Tarantool также используется во всё большем количестве проектов вне стен Mail.Ru. Это, к примеру, онлайн-игры, цифровой маркетинг, социальные сети. Несмотря на то что Mail.Ru спонсирует разработку Tarantool’а, весь процесс разработки, в т.ч. дальнейшие планы и база обнаруженных ошибок, является полностью открытым. В Tarantool включены патчи от большого числа сторонних разработчиков. Усилиями сообщества разработчиков Tarantool’а были написаны (и далее поддерживаются) библиотеки для подключения модулей на внешних языках программирования. А сообщество Lua-разработчиков предоставило сотни полезных пакетов, большинство из которых можно использовать в качестве расширений для Tarantool’а.

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

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

Возможности СУБД

Компонент «box» – серверная часть с функциями СУБД – это важная часть Tarantool’а, хотя он может работать и без данного компонента.

API для функций СУБД позволяет хранить Lua-объекты, управлять коллекциями объектов, создавать и удалять вторичные ключи, делать атомарные изменения, конфигурировать и мониторить репликацию, производить контролируемое переключение при отказе (failover), а также исполнять код на Lua, который вызывается событиями в базе. А для прозрачного доступа к удаленным (remote) экземплярам баз данных разработан API для вызова удаленных процедур.

В архитектуре серверной части СУБД Tarantool’а реализована концепция «движков» базы данных (storage engines), где в разных ситуациях используются разные наборы алгоритмов и структуры данных. В Tarantool’е есть два встроенных движка: in-memory движок, который держит все данные и индексы в оперативной памяти, и двухуровневый движок для B-деревьев, который обрабатывает данные размером в 10-1000 раз больше того, что может поместиться в оперативной памяти. Все движки в Tarantool’е поддерживают транзакции и репликацию, поскольку они используют единый механизм упреждающей записи (WAL = write ahead log). Это механизм обеспечивает согласованность и сохранность данных при сбоях. Таким образом, изменения не считаются завершенными, пока не проходит запись в лог WAL. Подсистема записи в журнал также поддерживает групповые коммиты.

In-memory движок базы данных Tarantool’а (memtx) хранит все данные в оперативной памяти, поэтому у него низкое значение задержки чтения. Кроме того, когда пользователи запрашивают снимки данных (snapshots), этот движок создает персистентные копии данных в энергонезависимой памяти, например на диске. Если экземпляр сервера прекращает работать и данные в оперативной памяти теряются, то при следующем запуске сервер загрузит в память самый свежий снимок и воспроизведет все транзакции из журнала. Таким образом, данные не теряются.

В штатных ситуациях in-memory движок работает без блокировок. Вместо многопоточных примитивов, которые предлагает операционная система (таких как mutex’ы), Tarantool использует кооперативную многозадачность для работы с тысячами соединений одновременно. В Tarantool’е есть фиксированное количество независимых потоков управления (thread), и у них нет общего состояния. Для обмена данными между потоками используются очереди сообщений с малой перегрузкой. Хотя такой подход накладывает ограничение на количество процессорных ядер, которые может использовать экземпляр, в то же время он позволяет избежать борьбы за шину памяти, а также дает запас масштабируемости по скорости доступа к памяти и производительности сети. В результате даже при большой нагрузке экземпляр Tarantool’а в среднем использует процессор менее чем на 10%. Кроме того, Tarantool поддерживает поиск как по первичным, так и по внешним ключам в индексах.

Дисковый движок базы данных Tarantool’а совмещает в себе подходы, заимствованные из современных файловых систем, журнально-структурированных деревьев со слиянием (log-structured merge trees) и классических B-деревьев. Все данные разбиты на диапазоны. Каждый диапазон представлен файлом на диске. Размер диапазона можно изменять, обычно он равен 64МБ. Каждый диапазон – это набор страниц, которые служат разным целям. После полного слияния диапазона ключи на его страницах не пересекаются. Если диапазоны ключей недавно сильно изменялись, можно провести частичное слияние диапазона. В этом случае на некоторых страницах появились новые ключи и значения. Дисковый движок обновляет данные по принципу дописывания в конец: новые данные никогда не затирают старые. Дисковый движок базы данных называется vinyl.

Tarantool поддерживает работу с составными ключами в индексах. Возможные типы ключей: HASH, TREE, BITSET и RTREE.

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

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

Предисловие

Добро пожаловать в мир 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, а также как создать простую базу данных.

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

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

For trial and test purposes, we recommend using official Tarantool images for Docker. An official image contains a particular Tarantool version (1.6, 1.9, 1.10 or 2.0) and all popular external modules for Tarantool. Everything is already installed and configured in Linux. These images are the easiest way to install and use Tarantool.

Примечание

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

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

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

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

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

This command runs a new container named „mytarantool“. Docker starts it from an official image named „tarantool/tarantool:1“, with Tarantool version 1.9 and all external modules already installed.

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}

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

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'}
                        > })

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

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

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

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

$ docker stop mytarantool

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

Использование бинарного пакета

For production purposes, we recommend official binary packages. You can choose from two Tarantool versions: 1.10 (stable) or 2.0 (alpha). An automatic build system creates, tests and publishes packages for every push into a corresponding branch (1.10 or 2.0) at Tarantool’s GitHub repository.

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

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

Для запуска экземпляра Tarantool’а выполните эту команду:

$ # если вы скачали бинарный пакет с помощью apt-get или yum, введите:
            $ /usr/bin/tarantool
            $ # если вы скачали бинарный пакет в формате TAR
            $ # и разархивировали его в директорию ~/tarantool, введите:
            $ ~/tarantool/bin/tarantool

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

tarantool>

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

Примечание

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

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

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

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

$ mkdir ~/tarantool_sandbox
            $ cd ~/tarantool_sandbox

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

tarantool> box.cfg{listen = 3301}

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

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

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

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

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

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

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

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

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

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

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]
          ...

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

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

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

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

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

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’ом в качестве системы управления базой данных.

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

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

В этом разделе описывается то, как в 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 = {1, '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’а.

But if you want the ordering that you see in phone books and dictionaries, then you need Tarantool’s optional collationsunicode and unicode_ci – that allow for 'a' < 'A' < 'B' and 'a' = 'A' < 'B' respectively.

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

  • unicode collation observes all weights, from L1 to Ln (identical),
  • unicode_ci collation observes only L1 weights, so for example „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

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

Для первоначального примера сгенерируем последовательность под названием „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}, {{'=', 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 или другому языку программирования. Клиент-серверный протокол открыт и задокументирован. См. БНФ с комментариями.

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

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

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

:samp:`box.space.{имя-спейса}:create_index('{имя-индекса}')`

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

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

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

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

Возможны следующие варианты SELECT:

  1. Помимо условия равенства, при поиске могут использоваться и другие условия сравнения.

    box.space.space-name:select(value, {iterator = 'GT'})
    

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

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

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

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

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

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

    -- Suppose an index has two parts
    tarantool> box.space.space-name.index.index-name.parts
    ---
    - - type: unsigned
        fieldno: 1
      - type: string
        fieldno: 2
    ...
    -- Suppose the space has three tuples
    box.space.space-name:select()
    ---
    - - [1, 'A']
      - [1, 'B']
      - [2, '']
    ...
    
  4. Поиск может производиться по всем полям с использованием таблицы значений:

    box.space.space-name:select({1, 'A'})
    

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

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

    Во втором случае Tarantool вернет два кортежа: {1, 'A'} и {1,  'B'}.

    При необходимости можно задать даже нулевые поля, в результате чего Tarantool вернет все три кортежа (обратите внимание, что поиск по компонентам ключа доступен только для TREE-индексов).

Примеры

  • Пример работы с 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

Этот запрос будет обработан тремя потоками операционной системы:

  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 использует простой планировщик с оптимистичным управлением: транзакция подтверждена первой – выигрывает. Если параллельная активная транзакция читает значение, измененное подтвержденной транзакцией, она прерывается.

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

Примечание

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

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

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

Операции по изменению базы данных обычно не передают управление, но это зависит от движка:

  • В memtx’е чтение и запись не требуют ввода-вывода и не передают управление.
  • В vinyl’е не все данные находятся в оперативной памяти, и запрос SELECT часто подразумевает дисковый ввод-вывод и, следовательно, передачу управления, пока запись ожидает освобождения памяти, что также вызывает передачу управления.

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

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

Пример №1

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

Пример №2

Предположим, что в спейсе ‘tester’ существуют кортежи, третье поле которых представляет собой положительную сумму в долларах. Начнем транзакцию, снимем сумму из кортежа №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-файл.

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

select
            select
            select

приводит к блокировке (в memtx’е), если находится внутри функции или Lua-программы, которая выполняется на экземпляре сервера. Однако она вызывает передачу управления (и в memtx’е, и в vinyl’е), если выполняется как серия передач от клиента, включая клиентов, работающих по telnet, по одному из коннекторов или модулей MySQL и PostgreSQL или в интерактивном режиме при использовании Tarantool’а как клиента.

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

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

Understanding security details is primarily an issue for administrators. However, ordinary users should at least skim this section to get an idea of how Tarantool makes it possible for administrators to prevent unauthorized access to the database and to certain functions.

Общая информация:

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

Далее рассмотрим эти пункты более подробно.

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

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

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

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

  • For a binary port connection – with the AUTH protocol command, supported by most clients;
  • Для соединения по порту для административной консоли и при выполнении скрипта инициализации на Lua – с помощью box.session.su;
  • For a binary-port connection invoking a stored function with the CALL command – if the SETUID property is enabled for the function, Tarantool temporarily replaces the current user with the function’s creator, with all the creator’s privileges, during function execution.

Пароли

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

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

Примечание

For more details of the password hashing algorithm (e.g. for the purpose of writing a new client application), read the scramble.h header file.

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

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

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

Tarantool has one database. It may be called «box.schema» or «universe». The database contains database objects, including spaces, indexes, users, roles, sequences, and functions.

The owner of a database object is the user who created it. The owner of the database itself, and the owner of objects that are created initially – the system spaces and the default users – is „admin“.

Owners automatically have privileges for what they create. They can share these privileges with other users or with roles, using box.schema.user.grant requests. The following privileges can be granted:

  • „read“, e.g. allow select from a space
  • „write“, e.g. allow update on a space
  • „execute“, e.g. allow call of a function
  • „create“, e.g. allow box.schema.space.create (access to certain system spaces is also necessary)
  • „alter“, e.g. allow box.space.x.index.y:alter (access to certain system spaces is also necessary)
  • „drop“, e.g. allow box.sequence.x:drop (currently this can be granted but has no effect)
  • „usage“, e.g. whether any action is allowable regardless of other privileges (sometimes revoking „usage“ is a convenient way to block a user temporarily without dropping the user)
  • „session“, e.g. whether the user can „connect“.

To create objects, users need the „create“ privilege and at least „read“ and „write“ privileges on the system space with a similar name (for example, on the _space if the user needs to create spaces).

To access objects, users need an appropriate privilege on the object (for example, the „execute“ privilege on function F if the users need to execute function F). See below some examples for granting specific privileges that a grantor – that is, „admin“ or the object creator – can make.

To drop an object, users must be the object’s creator or be „admin“. As the owner of the entire database, „admin“ can drop any object including other users.

To grant privileges to a user, the object owner says grant(). To revoke privileges from a user, the object owner says revoke(). In either case, there are three or four parameters:

(user-name, privilege, object-type [, object-name])
  • user-name is the user (or role) that will receive or lose the privilege;
  • privilege is any of „read“, „write“, „execute“, „create“, „alter“, „drop“, „usage“, or „session“ (or a comma-separated list);
  • object-type is any of „space“, „index“, „sequence“, „function“, role-name, or „universe“;
  • object-name is what the privilege is for (omitted if object-type is „universe“).

Example for granting many privileges at once

In this example user „admin“ grants many privileges on many objects to user „U“, with a single request.

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

Examples for granting privileges for specific operations

In these examples the object’s creator grants precisely the minimal privileges necessary for particular operations, to user „U“.

-- So that 'U' can create spaces:
  box.schema.user.grant('U','create','universe')
  box.schema.user.grant('U','write', 'space', '_schema')
  box.schema.user.grant('U','write', 'space', '_space')
-- So that 'U' can  create indexes (assuming 'U' created the space)
  box.schema.user.grant('U','read', 'space', '_space')
  box.schema.user.grant('U','read,write', 'space', '_index')
-- So that 'U' can  create indexes on space T (assuming 'U' did not create space 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')
-- So that 'U' can  alter indexes on space T (assuming 'U' did not create the index)
  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')
-- So that 'U' can create users or roles:
  box.schema.user.grant('U','create','universe')
  box.schema.user.grant('U','read,write', 'space', '_user')
  box.schema.user.grant('U','write','space', '_priv')
-- So that 'U' can create sequences:
  box.schema.user.grant('U','create','universe')
  box.schema.user.grant('U','read,write','space','_sequence')
-- So that 'U' can create functions:
  box.schema.user.grant('U','create','universe')
  box.schema.user.grant('U','read,write','space','_func')
-- So that 'U' can grant access on objects that 'U' created
  box.schema.user.grant('U','read','space','_user')
-- So that 'U' can select or get from a space named 'T'
  box.schema.user.grant('U','read','space','T')
-- So that 'U' can update or insert or delete or truncate a space named 'T'
  box.schema.user.grant('U','write','space','T')
-- So that 'U' can execute a function named 'F'
  box.schema.user.grant('U','execute','function','F')
-- So that 'U' can use the "S:next()" function with a sequence named S
  box.schema.user.grant('U','read,write','sequence','S')
-- So that 'U' can use the "S:set()" or "S:reset() function with a sequence named S
  box.schema.user.grant('U','write','sequence','S')

Example for creating users and objects then granting privileges

Здесь создадим 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. Другими словами, пользователь получает все права, которые предоставляются ролям пользователя напрямую и опосредованно.

The „usage“ and „session“ privileges cannot be granted to roles.

Пример

-- Этот пример сработает для пользователя со множеством прав, например, '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', 'execute', 'role', 'R2')
            box.schema.user.grant('U1', 'execute', 'role', '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}

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

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

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

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

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

Примечание

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

Триггеры

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

There are four types of triggers in Tarantool:

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

  • Triggers associate a function with an event. The request to «define a trigger» implies passing the trigger’s function to one of the «on_event()» functions:
  • Только пользователь „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_connect() – без аргументов – чтобы вернуть таблицу со всеми триггерными функциями для обработки соединений;
  • on_auth(), чтобы вернуть все триггерные функции для обработки аутентификации;
  • on_disconnect(), чтобы вернуть все триггерные функции для обработки отключений;
  • on_replace(), чтобы вернуть все триггерные функции для обработки замены, сделанные для on_replace().
  • before_replace(), чтобы вернуть все триггерные функции для обработки замены, сделанные для before_replace().

Пример

Здесь мы записываем события подключения и отключения в журнал на сервере 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).

Storage engines

A storage engine is a set of very-low-level routines which actually store and retrieve tuple values. Tarantool offers a choice of two storage engines:

  • memtx (the in-memory storage engine) is the default and was the first to arrive.

  • vinyl (the on-disk storage engine) is a working key-value engine and will especially appeal to users who like to see data go directly to disk, so that recovery time might be shorter and database size might be larger.

    On the other hand, vinyl lacks some functions and options that are available with memtx. Where that is the case, the relevant description in this manual contains a note beginning with the words «Note re storage engine».

Further in this section we discuss the details of storing data using the vinyl storage engine.

To specify that the engine should be vinyl, add the clause engine = 'vinyl' when creating a space, for example:

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

Differences between memtx and vinyl storage engines

The primary difference between memtx and vinyl is that memtx is an «in-memory» engine while vinyl is an «on-disk» engine. An in-memory storage engine is generally faster (each query is usually run under 1 ms), and the memtx engine is justifiably the default for Tarantool, but on-disk engine such as vinyl is preferable when the database is larger than the available memory and adding more memory is not a realistic option.

Option memtx vinyl
Supported index type TREE, HASH, RTREE or BITSET TREE
Temporary spaces Supported Not supported
random() function Supported Not supported
alter() function Supported Supported starting from the 1.10.2 release (the primary index cannot be modified)
len() function Returns the number of tuples in the space Returns the maximum approximate number of tuples in the space
count() function Takes a constant amount of time Takes a variable amount of time depending on a state of a DB
delete() function Returns the deleted tuple, if any Always returns nil
yield Does not yield on the select requests unless the transaction is commited to WAL Yields on the select requests or on its equivalents: get() or pairs()

Storing data with vinyl

Tarantool is a transactional and persistent DBMS that maintains 100% of its data in RAM. The greatest advantages of in-memory databases are their speed and ease of use: they demonstrate consistently high performance, but you never need to tune them.

A few years ago we decided to extend the product by implementing a classical storage engine similar to those used by regular DBMSes: it uses RAM for caching, while the bulk of its data is stored on disk. We decided to make it possible to set a storage engine independently for each table in the database, which is the same way that MySQL approaches it, but we also wanted to support transactions from the very beginning.

The first question we needed to answer was whether to create our own storage engine or use an existing library. The open-source community offered a few viable solutions. The RocksDB library was the fastest growing open-source library and is currently one of the most prominent out there. There were also several lesser-known libraries to consider, such as WiredTiger, ForestDB, NestDB, and LMDB.

Nevertheless, after studying the source code of existing libraries and considering the pros and cons, we opted for our own storage engine. One reason is that the existing third-party libraries expected requests to come from multiple operating system threads and thus contained complex synchronization primitives for controlling parallel data access. If we had decided to embed one of these in Tarantool, we would have made our users bear the overhead of a multithreaded application without getting anything in return. The thing is, Tarantool has an actor-based architecture. The way it processes transactions in a dedicated thread allows it to do away with the unnecessary locks, interprocess communication, and other overhead that accounts for up to 80% of processor time in multithreaded DBMSes.

../../../../_images/actor_threads.png

The Tarantool process consists of a fixed number of «actor» threads

If you design a database engine with cooperative multitasking in mind right from the start, it not only significantly speeds up the development process, but also allows the implementation of certain optimization tricks that would be too complex for multithreaded engines. In short, using a third-party solution wouldn’t have yielded the best result.

Algorithm

Once the idea of using an existing library was off the table, we needed to pick an architecture to build upon. There are two competing approaches to on-disk data storage: the older one relies on B-trees and their variations; the newer one advocates the use of log-structured merge-trees, or «LSM» trees. MySQL, PostgreSQL, and Oracle use B-trees, while Cassandra, MongoDB, and CockroachDB have adopted LSM trees.

B-trees are considered better suited for reads and LSM trees—for writes. However, with SSDs becoming more widespread and the fact that SSDs have read throughput that’s several times greater than write throughput, the advantages of LSM trees in most scenarios was more obvious to us.

Before dissecting LSM trees in Tarantool, let’s take a look at how they work. To do that, we’ll begin by analyzing a regular B-tree and the issues it faces. A B-tree is a balanced tree made up of blocks, which contain sorted lists of key- value pairs. (Topics such as filling and balancing a B-tree or splitting and merging blocks are outside of the scope of this article and can easily be found on Wikipedia). As a result, we get a container sorted by key, where the smallest element is stored in the leftmost node and the largest one in the rightmost node. Let’s have a look at how insertions and searches in a B-tree happen.

../../../../_images/classical_b_tree.png

Classical B-tree

If you need to find an element or check its membership, the search starts at the root, as usual. If the key is found in the root block, the search stops; otherwise, the search visits the rightmost block holding the largest element that’s not larger than the key being searched (recall that elements at each level are sorted). If the first level yields no results, the search proceeds to the next level. Finally, the search ends up in one of the leaves and probably locates the needed key. Blocks are stored and read into RAM one by one, meaning the algorithm reads blocks in a single search, where N is the number of elements in the B-tree. In the simplest case, writes are done similarly: the algorithm finds the block that holds the necessary element and updates (inserts) its value.

To better understand the data structure, let’s consider a practical example: say we have a B-tree with 100,000,000 nodes, a block size of 4096 bytes, and an element size of 100 bytes. Thus each block will hold up to 40 elements (all overhead considered), and the B-tree will consist of around 2,570,000 blocks and 5 levels: the first four will have a size of 256 Mb, while the last one will grow up to 10 Gb. Obviously, any modern computer will be able to store all of the levels except the last one in filesystem cache, so read requests will require just a single I/O operation.

But if we change our perspective —B-trees don’t look so good anymore. Suppose we need to update a single element. Since working with B-trees involves reading and writing whole blocks, we would have to read in one whole block, change our 100 bytes out of 4096, and then write the whole updated block to disk. In other words,we were forced to write 40 times more data than we actually modified!

If you take into account the fact that an SSD block has a size of 64 Kb+ and not every modification changes a whole element, the extra disk workload can be greater still.

Authors of specialized literature and blogs dedicated to on-disk data storage have coined two terms for these phenomena: extra reads are referred to as «read amplification» and writes as «write amplification».

The amplification factor (multiplication coefficient) is calculated as the ratio of the size of actual read (or written) data to the size of data needed (or actually changed). In our B-tree example, the amplification factor would be around 40 for both reads and writes.

The huge number of extra I/O operations associated with updating data is one of the main issues addressed by LSM trees. Let’s see how they work.

The key difference between LSM trees and regular B-trees is that LSM trees don’t just store data (keys and values), but also data operations: insertions and deletions.

../../../../_images/lsm.png


LSM tree:

  • Stores statements, not values:
    • REPLACE
    • DELETE
    • UPSERT
  • Every statement is marked by LSN Append-only files, garbage is collected after a checkpoint
  • Transactional log of all filesystem changes: vylog

For example, an element corresponding to an insertion operation has, apart from a key and a value, an extra byte with an operation code («REPLACE» in the image above). An element representing the deletion operation contains a key (since storing a value is unnecessary) and the corresponding operation code—»DELETE». Also, each LSM tree element has a log sequence number (LSN), which is the value of a monotonically increasing sequence that uniquely identifies each operation. The whole tree is first ordered by key in ascending order, and then, within a single key scope, by LSN in descending order.

../../../../_images/lsm_single.png

A single level of an LSM tree

Filling an LSM tree

Unlike a B-tree, which is stored completely on disk and can be partly cached in RAM, when using an LSM tree, memory is explicitly separated from disk right from the start. The issue of volatile memory and data persistence is beyond the scope of the storage algorithm and can be solved in various ways—for example, by logging changes.

The part of an LSM tree that’s stored in RAM is called L0 (level zero). The size of RAM is limited, so L0 is allocated a fixed amount of memory. For example, in Tarantool, the L0 size is controlled by the vinyl_memory parameter. Initially, when an LSM tree is empty, operations are written to L0. Recall that all elements are ordered by key in ascending order, and then within a single key scope, by LSN in descending order, so when a new value associated with a given key gets inserted, it’s easy to locate the older value and delete it. L0 can be structured as any container capable of storing a sorted sequence of elements. For example, in Tarantool, L0 is implemented as a B+*-tree. Lookups and insertions are standard operations for the data structure underlying L0, so I won’t dwell on those.

Sooner or later the number of elements in an LSM tree exceeds the L0 size and that’s when L0 gets written to a file on disk (called a «run») and then cleared for storing new elements. This operation is called a «dump».

../../../../_images/dumps.png


Dumps on disk form a sequence ordered by LSN: LSN ranges in different runs don’t overlap, and the leftmost runs (at the head of the sequence) hold newer operations. Think of these runs as a pyramid, with the newest ones closer to the top. As runs keep getting dumped, the pyramid grows higher. Note that newer runs may contain deletions or replacements for existing keys. To remove older data, it’s necessary to perform garbage collection (this process is sometimes called «merge» or «compaction») by combining several older runs into a new one. If two versions of the same key are encountered during a compaction, only the newer one is retained; however, if a key insertion is followed by a deletion, then both operations can be discarded.

../../../../_images/purge.png


The key choices determining an LSM tree’s efficiency are which runs to compact and when to compact them. Suppose an LSM tree stores a monotonically increasing sequence of keys (1, 2, 3, …,) with no deletions. In this case, compacting runs would be useless: all of the elements are sorted, the tree doesn’t have any garbage, and the location of any key can unequivocally be determined. On the other hand, if an LSM tree contains many deletions, doing a compaction would free up some disk space. However, even if there are no deletions, but key ranges in different runs overlap a lot, compacting such runs could speed up lookups as there would be fewer runs to scan. In this case, it might make sense to compact runs after each dump. But keep in mind that a compaction causes all data stored on disk to be overwritten, so with few reads it’s recommended to perform it less often.

To ensure it’s optimally configurable for any of the scenarios above, an LSM tree organizes all runs into a pyramid: the newer the data operations, the higher up the pyramid they are located. During a compaction, the algorithm picks two or more neighboring runs of approximately equal size, if possible.

../../../../_images/compaction.png


  • Multi-level compaction can span any number of levels
  • A level can contain multiple runs

All of the neighboring runs of approximately equal size constitute an LSM tree level on disk. The ratio of run sizes at different levels determines the pyramid’s proportions, which allows optimizing the tree for write-intensive or read-intensive scenarios.

Suppose the L0 size is 100 Mb, the ratio of run sizes at each level (the vinyl_run_size_ratio parameter) is 5, and there can be no more than 2 runs per level (the vinyl_run_count_per_level parameter). After the first 3 dumps, the disk will contain 3 runs of 100 Mb each—which constitute L1 (level one). Since 3 > 2, the runs will be compacted into a single 300 Mb run, with the older ones being deleted. After 2 more dumps, there will be another compaction, this time of 2 runs of 100 Mb each and the 300 Mb run, which will produce one 500 Mb run. It will be moved to L2 (recall that the run size ratio is 5), leaving L1 empty. The next 10 dumps will result in L2 having 3 runs of 500 Mb each, which will be compacted into a single 1500 Mb run. Over the course of 10 more dumps, the following will happen: 3 runs of 100 Mb each will be compacted twice, as will two 100 Mb runs and one 300 Mb run, which will yield 2 new 500 Mb runs in L2. Since L2 now has 3 runs, they will also be compacted: two 500 Mb runs and one 1500 Mb run will produce a 2500 Mb run that will be moved to L3, given its size.

This can go on infinitely, but if an LSM tree contains lots of deletions, the resulting compacted run can be moved not only down, but also up the pyramid due to its size being smaller than the sizes of the original runs that were compacted. In other words, it’s enough to logically track which level a certain run belongs to, based on the run size and the smallest and greatest LSN among all of its operations.

Controlling the form of an LSM tree

If it’s necessary to reduce the number of runs for lookups, then the run size ratio can be increased, thus bringing the number of levels down. If, on the other hand, you need to minimize the compaction-related overhead, then the run size ratio can be decreased: the pyramid will grow higher, and even though runs will be compacted more often, they will be smaller, which will reduce the total amount of work done. In general, write amplification in an LSM tree is described by this formula: or, alternatively, , where N is the total size of all tree elements, L0 is the level zero size, and x is the level size ratio (the level_size_ratio parameter). At = 40 (the disk-to- memory ratio), the plot would look something like this:

../../../../_images/curve.png


As for read amplification, it’s proportional to the number of levels. The lookup cost at each level is no greater than that for a B-tree. Getting back to the example of a tree with 100,000,000 elements: given 256 Mb of RAM and the default values of vinyl_level_size_ratio and run_count_per_level, write amplification would come out to about 13, while read amplification could be as high as 150. Let’s try to figure out why this happens.

Range searching

In the case of a single-key search, the algorithm stops after encountering the first match. However, when searching within a certain key range (for example, looking for all the users with the last name «Ivanov»), it’s necessary to scan all tree levels.

../../../../_images/range_search.png

Searching within a range of [24,30)

The required range is formed the same way as when compacting several runs: the algorithm picks the key with the largest LSN out of all the sources, ignoring the other associated operations, then moves on to the next key and repeats the procedure.

Deletion

Why would one store deletions? And why doesn’t it lead to a tree overflow in the case of for i=1,10000000 put(i) delete(i) end?

With regards to lookups, deletions signal the absence of a value being searched; with compactions, they clear the tree of «garbage» records with older LSNs.

While the data is in RAM only, there’s no need to store deletions. Similarly, you don’t need to keep them following a compaction if they affect, among other things, the lowest tree level, which contains the oldest dump. Indeed, if a value can’t be found at the lowest level, then it doesn’t exist in the tree.

  • We can’t delete from append-only files
  • Tombstones (delete markers) are inserted into L0 instead
../../../../_images/deletion_1.png

Deletion, step 1: a tombstone is inserted into L0

../../../../_images/deletion_2.png

Deletion, step 2: the tombstone passes through intermediate levels

../../../../_images/deletion_3.png

Deletion, step 3: in the case of a major compaction, the tombstone is removed from the tree

If a deletion is known to come right after the insertion of a unique value, which is often the case when modifying a value in a secondary index, then the deletion can safely be filtered out while compacting intermediate tree levels. This optimization is implemented in vinyl.

Advantages of an LSM tree

Apart from decreasing write amplification, the approach that involves periodically dumping level L0 and compacting levels L1-Lk has a few advantages over the approach to writes adopted by B-trees:

  • Dumps and compactions write relatively large files: typically, the L0 size is 50-100 Mb, which is thousands of times larger than the size of a B-tree block.
  • This large size allows efficiently compressing data before writing it. Tarantool compresses data automatically, which further decreases write amplification.
  • There is no fragmentation overhead, since there’s no padding/empty space between the elements inside a run.
  • All operations create new runs instead of modifying older data in place. This allows avoiding those nasty locks that everyone hates so much. Several operations can run in parallel without causing any conflicts. This also simplifies making backups and moving data to replicas.
  • Storing older versions of data allows for the efficient implementation of transaction support by using multiversion concurrency control.
Disadvantages of an LSM tree and how to deal with them

One of the key advantages of the B-tree as a search data structure is its predictability: all operations take no longer than to run. Conversely, in a classical LSM tree, both read and write speeds can differ by a factor of hundreds (best case scenario) or even thousands (worst case scenario). For example, adding just one element to L0 can cause it to overflow, which can trigger a chain reaction in levels L1, L2, and so on. Lookups may find the needed element in L0 or may need to scan all of the tree levels. It’s also necessary to optimize reads within a single level to achieve speeds comparable to those of a B-tree. Fortunately, most disadvantages can be mitigated or even eliminated with additional algorithms and data structures. Let’s take a closer look at these disadvantages and how they’re dealt with in Tarantool.

Unpredictable write speed

In an LSM tree, insertions almost always affect L0 only. How do you avoid idle time when the memory area allocated for L0 is full?

Clearing L0 involves two lengthy operations: writing to disk and memory deallocation. To avoid idle time while L0 is being dumped, Tarantool uses writeaheads. Suppose the L0 size is 256 Mb. The disk write speed is 10 Mbps. Then it would take 26 seconds to dump L0. The insertion speed is 10,000 RPS, with each key having a size of 100 bytes. While L0 is being dumped, it’s necessary to reserve 26 Mb of RAM, effectively slicing the L0 size down to 230 Mb.

Tarantool does all of these calculations automatically, constantly updating the rolling average of the DBMS workload and the histogram of the disk speed. This allows using L0 as efficiently as possible and it prevents write requests from timing out. But in the case of workload surges, some wait time is still possible. That’s why we also introduced an insertion timeout (the vinyl_timeout parameter), which is set to 60 seconds by default. The write operation itself is executed in dedicated threads. The number of these threads (2 by default) is controlled by the vinyl_write_threads parameter. The default value of 2 allows doing dumps and compactions in parallel, which is also necessary for ensuring system predictability.

In Tarantool, compactions are always performed independently of dumps, in a separate execution thread. This is made possible by the append-only nature of an LSM tree: after dumps runs are never changed, and compactions simply create new runs.

Delays can also be caused by L0 rotation and the deallocation of memory dumped to disk: during a dump, L0 memory is owned by two operating system threads, a transaction processing thread and a write thread. Even though no elements are being added to the rotated L0, it can still be used for lookups. To avoid read locks when doing lookups, the write thread doesn’t deallocate the dumped memory, instead delegating this task to the transaction processor thread. Following a dump, memory deallocation itself happens instantaneously: to achieve this, L0 uses a special allocator that deallocates all of the memory with a single operation.

../../../../_images/dump_from_shadow.png
  • anticipatory dump
  • throttling

The dump is performed from the so-called «shadow» L0 without blocking new insertions and lookups

Unpredictable read speed

Optimizing reads is the most difficult optimization task with regards to LSM trees. The main complexity factor here is the number of levels: any optimization causes not only much slower lookups, but also tends to require significantly larger RAM resources. Fortunately, the append-only nature of LSM trees allows us to address these problems in ways that would be nontrivial for traditional data structures.

../../../../_images/read_speed.png
  • page index
  • bloom filters
  • tuple range cache
  • multi-level compaction
Compression and page index

In B-trees, data compression is either the hardest problem to crack or a great marketing tool—rather than something really useful. In LSM trees, compression works as follows:

During a dump or compaction all of the data within a single run is split into pages. The page size (in bytes) is controlled by the vinyl_page_size parameter and can be set separately for each index. A page doesn’t have to be exactly of vinyl_page_size size—depending on the data it holds, it can be a little bit smaller or larger. Because of this, pages never have any empty space inside.

Data is compressed by Facebook’s streaming algorithm called «zstd». The first key of each page, along with the page offset, is added to a «page index», which is a separate file that allows the quick retrieval of any page. After a dump or compaction, the page index of the created run is also written to disk.

All .index files are cached in RAM, which allows finding the necessary page with a single lookup in a .run file (in vinyl, this is the extension of files resulting from a dump or compaction). Since data within a page is sorted, after it’s read and decompressed, the needed key can be found using a regular binary search. Decompression and reads are handled by separate threads, and are controlled by the vinyl_read_threads parameter.

Tarantool uses a universal file format: for example, the format of a .run file is no different from that of an .xlog file (log file). This simplifies backup and recovery as well as the usage of external tools.

Bloom filters

Even though using a page index enables scanning fewer pages per run when doing a lookup, it’s still necessary to traverse all of the tree levels. There’s a special case, which involves checking if particular data is absent when scanning all of the tree levels and it’s unavoidable: I’m talking about insertions into a unique index. If the data being inserted already exists, then inserting the same data into a unique index should lead to an error. The only way to throw an error in an LSM tree before a transaction is committed is to do a search before inserting the data. Such reads form a class of their own in the DBMS world and are called «hidden» or «parasitic» reads.

Another operation leading to hidden reads is updating a value in a field on which a secondary index is defined. Secondary keys are regular LSM trees that store differently ordered data. In most cases, in order not to have to store all of the data in all of the indexes, a value associated with a given key is kept in whole only in the primary index (any index that stores both a key and a value is called «covering» or «clustered»), whereas the secondary index only stores the fields on which a secondary index is defined, and the values of the fields that are part of the primary index. Thus, each time a change is made to a value in a field on which a secondary index is defined, it’s necessary to first remove the old key from the secondary index—and only then can the new key be inserted. At update time, the old value is unknown, and it is this value that needs to be read in from the primary key «under the hood».

Например:

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

To minimize the number of disk reads, especially for nonexistent data, nearly all LSM trees use probabilistic data structures, and Tarantool is no exception. A classical Bloom filter is made up of several (usually 3-to-5) bit arrays. When data is written, several hash functions are calculated for each key in order to get corresponding array positions. The bits at these positions are then set to 1. Due to possible hash collisions, some bits might be set to 1 twice. We’re most interested in the bits that remain 0 after all keys have been added. When looking for an element within a run, the same hash functions are applied to produce bit positions in the arrays. If any of the bits at these positions is 0, then the element is definitely not in the run. The probability of a false positive in a Bloom filter is calculated using Bayes’ theorem: each hash function is an independent random variable, so the probability of a collision simultaneously occurring in all of the bit arrays is infinitesimal.

The key advantage of Bloom filters in Tarantool is that they’re easily configurable. The only parameter that can be specified separately for each index is called bloom_fpr (FPR stands for «false positive ratio») and it has the default value of 0.05, which translates to a 5% FPR. Based on this parameter, Tarantool automatically creates Bloom filters of the optimal size for partial- key and full-key searches. The Bloom filters are stored in the .index file, along with the page index, and are cached in RAM.

Caching

A lot of people think that caching is a silver bullet that can help with any performance issue. «When in doubt, add more cache». In vinyl, caching is viewed rather as a means of reducing the overall workload and consequently, of getting a more stable response time for those requests that don’t hit the cache. vinyl boasts a unique type of cache among transactional systems called a «range tuple cache». Unlike, say, RocksDB or MySQL, this cache doesn’t store pages, but rather ranges of index values obtained from disk, after having performed a compaction spanning all tree levels. This allows the use of caching for both single-key and key-range searches. Since this method of caching stores only hot data and not, say, pages (you may need only some data from a page), RAM is used in the most efficient way possible. The cache size is controlled by the vinyl_cache parameter.

Garbage collection control

Chances are that by now you’ve started losing focus and need a well-deserved dopamine reward. Feel free to take a break, since working through the rest of the article is going to take some serious mental effort.

An LSM tree in vinyl is just a small piece of the puzzle. Even with a single table (or so-called «space»), vinyl creates and maintains several LSM trees, one for each index. But even a single index can be comprised of dozens of LSM trees. Let’s try to understand why this might be necessary.

Recall our example with a tree containing 100,000,000 records, 100 bytes each. As time passes, the lowest LSM level may end up holding a 10 Gb run. During compaction, a temporary run of approximately the same size will be created. Data at intermediate levels takes up some space as well, since the tree may store several operations associated with a single key. In total, storing 10 Gb of actual data may require up to 30 Gb of free space: 10 Gb for the last tree level, 10 Gb for a temporary run, and 10 Gb for the remaining data. But what if the data size is not 10 Gb, but 1 Tb? Requiring that the available disk space always be several times greater than the actual data size is financially unpractical, not to mention that it may take dozens of hours to create a 1 Tb run. And in the case of an emergency shutdown or system restart, the process would have to be started from scratch.

Here’s another scenario. Suppose the primary key is a monotonically increasing sequence—for example, a time series. In this case, most insertions will fall into the right part of the key range, so it wouldn’t make much sense to do a compaction just to append a few million more records to an already huge run.

But what if writes predominantly occur in a particular region of the key range, whereas most reads take place in a different region? How do you optimize the form of the LSM tree in this case? If it’s too high, read performance is impacted; if it’s too low—write speed is reduced.

Tarantool «factorizes» this problem by creating multiple LSM trees for each index. The approximate size of each subtree is controlled by the vinyl_range_size parameter, which is equal to 1 Gb by default. We call such subtrees «ranges».

../../../../_images/factor_lsm.png


Factorizing large LSM trees via ranging

  • Ranges reflect a static layout of sorted runs
  • Slices connect a sorted run into a range

Initially, when the index has few elements, it consists of a single range. As it gets filled, its total volume may exceed vinyl_range_size, in which case a special operation called «split» divides the tree into two equal parts. The tree is split at the middle element in the range of keys stored in the tree. For example, if the tree initially stores the full range of -inf…+inf, then after splitting it at the middle key X, we get two subtrees: one that stores the range of -inf…X, and the other storing the range of X…+inf. With this approach, we always know which subtree to use for writes and which one for reads. If the tree contained deletions and each of the neighboring ranges grew smaller as a result, the opposite operation called «coalesce» combines two neighboring trees into one.

Split and coalesce don’t entail a compaction, the creation of new runs, or other resource-intensive operations. An LSM tree is just a collection of runs. vinyl has a special metadata log that helps keep track of which run belongs to which subtree(s). This has the .vylog extension and its format is compatible with an .xlog file. Similarly to an .xlog file, the metadata log gets rotated at each checkpoint. To avoid the creation of extra runs with split and coalesce, we have also introduced an auxiliary entity called «slice». It’s a reference to a run containing a key range and it’s stored only in the metadata log. Once the reference counter drops to zero, the corresponding file gets removed. When it’s necessary to perform a split or to coalesce, Tarantool creates slice objects for each new tree, removes older slices, and writes these operations to the metadata log, which literally stores records that look like this: <tree id, slice id> or <slice id, run id, min, max>.

This way all of the heavy lifting associated with splitting a tree into two subtrees is postponed until a compaction and then is performed automatically. A huge advantage of dividing all of the keys into ranges is the ability to independently control the L0 size as well as the dump and compaction processes for each subtree, which makes these processes manageable and predictable. Having a separate metadata log also simplifies the implementation of both «truncate» and «drop». In vinyl, they’re processed instantly, since they only work with the metadata log, while garbage collection is done in the background.

Advanced features of vinyl
Upsert

In the previous sections, we mentioned only two operations stored by an LSM tree: deletion and replacement. Let’s take a look at how all of the other operations can be represented. An insertion can be represented via a replacement—you just need to make sure there are no other elements with the specified key. To perform an update, it’s necessary to read the older value from the tree, so it’s easier to represent this operation as a replacement as well—this speeds up future read requests by the key. Besides, an update must return the new value, so there’s no avoiding hidden reads.

In B-trees, the cost of hidden reads is negligible: to update a block, it first needs to be read from disk anyway. Creating a special update operation for an LSM tree that doesn’t cause any hidden reads is really tempting.

Such an operation must contain not only a default value to be inserted if a key has no value yet, but also a list of update operations to perform if a value does exist.

At transaction execution time, Tarantool just saves the operation in an LSM tree, then «executes» it later, during a compaction.

The upsert operation:

space:upsert(tuple, {{operator, field, value}, ... })
  • Non-reading update or insert
  • Delayed execution
  • Background upsert squashing prevents upserts from piling up

Unfortunately, postponing the operation execution until a compaction doesn’t leave much leeway in terms of error handling. That’s why Tarantool tries to validate upserts as fully as possible before writing them to an LSM tree. However, some checks are only possible with older data on hand, for example when the update operation is trying to add a number to a string or to remove a field that doesn’t exist.

A semantically similar operation exists in many products including PostgreSQL and MongoDB. But anywhere you look, it’s just syntactic sugar that combines the update and replace operations without avoiding hidden reads. Most probably, the reason is that LSM trees as data storage structures are relatively new.

Even though an upsert is a very important optimization and implementing it cost us a lot of blood, sweat, and tears, we must admit that it has limited applicability. If a table contains secondary keys or triggers, hidden reads can’t be avoided. But if you have a scenario where secondary keys are not required and the update following the transaction completion will certainly not cause any errors, then the operation is for you.

I’d like to tell you a short story about an upsert. It takes place back when vinyl was only beginning to «mature» and we were using an upsert in production for the first time. We had what seemed like an ideal environment for it: we had tons of keys, the current time was being used as values; update operations were inserting keys or modifying the current time; and we had few reads. Load tests yielded great results.

Nevertheless, after a couple of days, the Tarantool process started eating up 100% of our CPU, and the system performance dropped close to zero.

We started digging into the issue and found out that the distribution of requests across keys was significantly different from what we had seen in the test environment. It was…well, quite nonuniform. Most keys were updated once or twice a day, so the database was idle for the most part, but there were much hotter keys with tens of thousands of updates per day. Tarantool handled those just fine. But in the case of lookups by key with tens of thousands of upserts, things quickly went downhill. To return the most recent value, Tarantool had to read and «replay» the whole history consisting of all of the upserts. When designing upserts, we had hoped this would happen automatically during a compaction, but the process never even got to that stage: the L0 size was more than enough, so there were no dumps.

We solved the problem by adding a background process that performed readaheads on any keys that had more than a few dozen upserts piled up, so all those upserts were squashed and substituted with the read value.

Secondary keys

Update is not the only operation where optimizing hidden reads is critical. Even the replace operation, given secondary keys, has to read the older value: it needs to be independently deleted from the secondary indexes, and inserting a new element might not do this, leaving some garbage behind.

../../../../_images/secondary.png


If secondary indexes are not unique, then collecting «garbage» from them can be put off until a compaction, which is what we do in Tarantool. The append-only nature of LSM trees allowed us to implement full-blown serializable transactions in vinyl. Read-only requests use older versions of data without blocking any writes. The transaction manager itself is fairly simple for now: in classical terms, it implements the MVTO (multiversion timestamp ordering) class, whereby the winning transaction is the one that finished earlier. There are no locks and associated deadlocks. Strange as it may seem, this is a drawback rather than an advantage: with parallel execution, you can increase the number of successful transactions by simply holding some of them on lock when necessary. We’re planning to improve the transaction manager soon. In the current release, we focused on making the algorithm behave 100% correctly and predictably. For example, our transaction manager is one of the few on the NoSQL market that supports so-called «gap locks».

Сервер приложений

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

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

Запуск приложения

Используя Tarantool в качестве сервера приложений, вы можете написать собственные приложения. Собственный язык Tarantool’а для приложений – Lua, поэтому типовое приложение представляет собой файл, который содержит Lua-скрипт. Однако вы также можете писать приложения на C или C++.

Примечание

Если вы только осваиваете Lua, рекомендуем выполнить практическое задание по Tarantool’у до работы с данной главой. Для запуска практического задания, выполните команду tutorial() в консоли Tarantool’а:

tarantool> tutorial()
 ---
 - |
  Tutorial -- Screen #1 -- Hello, Moon
  ====================================

  Welcome to the Tarantool tutorial.
  It will introduce you to Tarantool’s Lua application server
  and database server, which is what’s running what you’re seeing.
  This is INTERACTIVE -- you’re expected to enter requests
  based on the suggestions or examples in the screen’s text.
  <...>

Создадим и запустим первое приложение на языке Lua для Tarantool’а – самое простое приложение, старую добрую программу «Hello, world!»:

#!/usr/bin/env tarantool
 print('Hello, world!')

Сохраним приложение в файле. Пусть это будет myapp.lua в текущей директории.

Теперь рассмотрим, как можно запустить наше приложение с Tarantool’ом.

Запуск в Docker

If we run Tarantool in a Docker container, the following command will start Tarantool 1.9 without any application:

$ # create a temporary container and run it in interactive mode
$ docker run --rm -t -i tarantool/tarantool:1

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

$ # create a temporary container and
$ # launch Tarantool with our application
$ docker run --rm -t -i \
             -v `pwd`/myapp.lua:/opt/tarantool/myapp.lua \
             -v /data/dir/on/host:/var/lib/tarantool \
             tarantool/tarantool:1 tarantool /opt/tarantool/myapp.lua

Здесь два ресурса подключаются к серверу в контейнере:

  • наш файл с приложением (\`pwd\`/myapp.lua) и
  • каталог данных Tarantool’а (/data/dir/on/host).

Традиционно в контейнере директория /opt/tarantool используется для кода приложения Tarantool’а, а директория /var/lib/tarantool используется для данных.

Запуск бинарной программы

При запуске Tarantool’а из бинарного пакета или сборке из исходников, можно запустить наше приложение:

  • в режиме скрипта,
  • как серверное приложение или
  • как демон службы.

Самый простой способ – передать имя файла в Tarantool при запуске:

$ tarantool myapp.lua
 Hello, world!
 $

Tarantool запускается, выполняет наш скрипт в режиме скрипта и завершает работу.

Теперь превратим этот скрипт в серверное приложение. Используем box.cfg из встроенного в Tarantool Lua-модуля, чтобы:

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

Также добавим простую логику для базы данных, используя space.create() и create_index() для создания спейса с первичным индексом. Используем функцию box.once(), чтобы обеспечить единовременное выполнение логики после первоначальной инициализации базы данных, поскольку мы не хотим создавать уже существующий спейс или индекс при каждом обращении к скрипту:

#!/usr/bin/env tarantool
            -- настроить базу данных
            box.cfg {
               listen = 3301
            }
            box.once("bootstrap", function()
               box.schema.space.create('tweedledum')
               box.space.tweedledum:create_index('primary',
                   { type = 'TREE', parts = {1, 'unsigned'}})
            end)

Далее запустим наше приложение, как делали ранее:

$ tarantool myapp.lua
 Hello, world!
 2016-12-19 16:07:14.250 [41436] main/101/myapp.lua C> version 1.7.2-146-g021d36b
 2016-12-19 16:07:14.250 [41436] main/101/myapp.lua C> log level 5
 2016-12-19 16:07:14.251 [41436] main/101/myapp.lua I> mapping 1073741824 bytes for tuple arena...
 2016-12-19 16:07:14.255 [41436] main/101/myapp.lua I> recovery start
 2016-12-19 16:07:14.255 [41436] main/101/myapp.lua I> recovering from `./00000000000000000000.snap'
 2016-12-19 16:07:14.271 [41436] main/101/myapp.lua I> recover from `./00000000000000000000.xlog'
 2016-12-19 16:07:14.271 [41436] main/101/myapp.lua I> done `./00000000000000000000.xlog'
 2016-12-19 16:07:14.272 [41436] main/102/hot_standby I> recover from `./00000000000000000000.xlog'
 2016-12-19 16:07:14.274 [41436] iproto/102/iproto I> binary: started
 2016-12-19 16:07:14.275 [41436] iproto/102/iproto I> binary: bound to [::]:3301
 2016-12-19 16:07:14.275 [41436] main/101/myapp.lua I> done `./00000000000000000000.xlog'
 2016-12-19 16:07:14.278 [41436] main/101/myapp.lua I> ready to accept requests

На этот раз Tarantool выполняет скрипт и продолжает работать в качестве сервера, принимая TCP-запросы на порт 3301. Можно увидеть Tarantool в списке процессов текущей сессии:

$ ps | grep "tarantool"
              PID TTY           TIME CMD
            41608 ttys001       0:00.47 tarantool myapp.lua <running>

Однако экземпляр Tarantool’а завершит работу, если мы закроем окно командной строки. Чтобы отделить Tarantool и приложение от окна командной строки, можно запустить режим демона. Для этого добавим некоторые параметры в box.cfg{}:

  • background = true, который собственно заставит Tarantool работать в качестве демона,
  • log = 'dir-name', который укажет, где демон Tarantool’а будет сохранять файл журнала (другие настройки журнала находятся в модуле Tarantool’а log module), а также
  • pid_file = 'file-name', который укажет, где демон Tarantool’а будет сохранять файл журнала pid-файл.

Например:

box.cfg {
             listen = 3301
             background = true,
             log = '1.log',
             pid_file = '1.pid'
          }

Запустим наше приложение, как делали ранее:

$ tarantool myapp.lua
 Hello, world!
 $

Tarantool выполняет наш скрипт, отделяется от текущей сессии (он не отображается при вводе ps | grep "tarantool") и продолжает работать в фоновом режиме в качестве демона, прикрепленного к общей сессии (с SID = 0):

$ ps -ef | grep "tarantool"
              PID SID     TIME  CMD
            42178   0  0:00.72 tarantool myapp.lua <running>

Рассмотрев создание и запуск Lua-приложения для Tarantool’а, перейдем к углубленному изложению методик программирования.

Создание приложения

Далее мы пошагово разберем ключевые методики программирования, что послужит хорошим началом для написания Lua-приложений для Tarantool’а. Для интереса возьмем историю реализации… настоящего микросервиса на основе Tarantool’а! Мы реализуем бэкенд для упрощенной версии Pokémon Go, игры на основе определения местоположения дополненной реальности, выпущенной в середине 2016 года. В этой игре игроки используют GPS-возможности мобильных устройств, чтобы находить, захватывать, сражаться и тренировать виртуальных существ, или покемонов, которые появляются на экране, как если бы они находились в том же реальном месте, как и игрок.

Чтобы не выходить за рамки пошагового примера, ограничим оригинальный сюжет игры. У нас есть карта с местами появления покемонов. Далее у нас есть несколько игроков, которые могут отправлять запросы на поимку покемона на сервер (где работает микросервис Tarantool’а). Сервер отвечает, пойман ли покемон, увеличивает счетчик покемонов, если пойман, и вызывает метод респауна покемона, который через некоторое время создает нового покемона на том же самом месте.

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

../../../../_images/aster.svg

Для начала как лучше всего предоставить микросервис?

Модули и приложения

Чтобы наша логическая схема игры была доступна другим разработчикам и Lua-приложениям, поместим ее в Lua-модуль.

Модуль (который называется «rock» в Lua) – это дополнительная библиотека, которая расширяет функции Tarantool’а. Поэтому можно установить нашу логическую схему в виде модуля в Tarantool и использовать ее из любого Tarantool-приложения или модуля. Как и приложения, модули в Tarantool’е могут быть написаны на Lua (rocks), C или C++.

Модули хороши для двух целей:

  • облегченное управление кодом (переиспользование, подготовка к развертыванию, версионирование) и
  • горячая перезагрузка кода без перезапуска экземпляра Tarantool’а.

В техническом смысле, модуль - это файл с исходным кодом, который экспортирует свои функции в API. Например, вот Lua-модуль под названием mymodule.lua, который экспортирует одну функцию под названием myfun:

local exports = {}
 exports.myfun = function(input_string)
    print('Hello', input_string)
 end
 return exports

Чтобы запустить функцию myfun() – из другого модуля, из Lua-приложения или из самого Tarantool’а – необходимо сохранить этот модуль в виде файла, а затем загрузить этот модуль с директивой require() и вызвать экспортированную функцию.

Например, вот Lua-приложение, которое использует функцию myfun() из модуля mymodule.lua:

-- загрузка модуля
            local mymodule = require('mymodule')

            -- вызов myfun() из функции test()
            local test = function()
              mymodule.myfun()
            end

Здесь важно запомнить, что директива require() берет пути загрузки к Lua-модулям из переменной package.path. Она представляет собой строку с разделителями в виде точки с запятой, где знак вопроса используется для вставки имени модуля. По умолчанию, эта переменная содержит пути в системе и рабочую директорию. Но если мы поместим наши модули в особую папку (например, scripts/), необходимо будет добавить эту папку в package.path до вызова require():

package.path = 'scripts/?.lua;' .. package.path

Для нашего микросервиса простым и удобным решением будет разместить все методы в Lua-модуле (скажем, pokemon.lua) и написать Lua-приложение (скажем, game.lua), которое запустит игровое окружение и цикл игры.

../../../../_images/aster.svg

Теперь приступим к деталям реализации. В игре нам необходимы три сущности:

  • карта, которая представляет собой массив покемонов с координатами мест респауна; в данной версии игры пусть местом будет прямоугольник, установленный по двум точкам, верхней левой и нижней правой;
  • игрок, у которого есть ID, имя и координаты местонахождения игрока;
  • покемон, у которого такие же поля, как и у игрока, плюс статус (активный/неактивный, то есть находится ли на карте) и возможность поимки (давайте уж дадим нашим покемонам шанс сбежать :-) )

Эти данные будем хранить как кортежи в спейсах Tarantool’а. Но чтобы бэкенд-приложение работало как микросервис, правильно будет отправлять/получать данные в универсальном формате JSON, используя Tarantool в качестве системы хранения документов.

Avro-схемы

Чтобы хранить JSON-данные в виде кортежей, используем продвинутую методику, которая уменьшит отпечаток данных и обеспечит пригодность всех сохраняемых документов. Будем использовать Tarantool-модуль avro-schema, который проверяет схему JSON-документа и конвертирует его в кортеж Tarantool’а. Кортеж будет содержать только значения полей, таким образом, занимая меньше места, чем оригинальный документ. С точки зрения avro-схемы, конвертация JSON-документов в кортежи – «flattening» (конвертация в плоские файлы), а восстановление оригинальных документов – «unflattening» (конвертация из плоских файлов). Использовать модуль достаточно просто:

  1. Для каждой сущности необходимо определить схему в синтаксисе схемы Apache Avro, где мы перечисляем поля сущности с их наименованиями и типами данных по Avro.
  2. При инициализации мы вызываем функцию avro-schema.create(), которая создает объекты в памяти для всех сущностей схемы, а также функцию compile(), которая создает методы flatten/unflatten (конвертация в плоские файлы и обратно) для каждой сущности.
  3. Далее мы просто вызываем методы flatten/unflatten для соответствующей сущности при получении/отправке данных об этой сущности.

Вот как будут выглядеть определения схемы для сущностей игрока и покемона:

local schema = {
     player = {
         type="record",
         name="player_schema",
         fields={
             {name="id", type="long"},
             {name="name", type="string"},
             {
                 name="location",
                 type= {
                     type="record",
                     name="player_location",
                     fields={
                         {name="x", type="double"},
                         {name="y", type="double"}
                     }
                 }
             }
         }
     },
     pokemon = {
         type="record",
         name="pokemon_schema",
         fields={
             {name="id", type="long"},
             {name="status", type="string"},
             {name="name", type="string"},
             {name="chance", type="double"},
             {
                 name="location",
                 type= {
                     type="record",
                     name="pokemon_location",
                     fields={
                         {name="x", type="double"},
                         {name="y", type="double"}
                     }
                 }
             }
         }
     }
 }

А вот как мы создадим и скомпилируем наши сущности при инициализации:

-- загрузить модуль avro-schema с директивой require()
 local avro = require('avro_schema')

 -- создать модели
 local ok_m, pokemon = avro.create(schema.pokemon)
 local ok_p, player = avro.create(schema.player)
 if ok_m and ok_p then
     -- скомпилировать модели
     local ok_cm, compiled_pokemon = avro.compile(pokemon)
     local ok_cp, compiled_player = avro.compile(player)
     if ok_cm and ok_cp then
         -- начать игру
         <...>
     else
         log.error('Schema compilation failed')
     end
 else
     log.info('Schema creation failed')
 end
 return false

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

../../../../_images/aster.svg

Далее нам нужны методы для реализации игровой логики. Чтобы смоделировать объектно-ориентированное программирование в нашем Lua-коде, будем хранить все Lua-функции и общие переменные в одной внутренней переменной (назовем ее game). Это позволит нам обращаться к функциям или переменным из нашего модуля с помощью self.func_name или self.var_name следующим образом:

local game = {
                -- локальная переменная
                num_players = 0,

                -- метод, который выводит локальную переменную
                hello = function(self)
                  print('Hello! Your player number is ' .. self.num_players .. '.')
                end,

                -- метод, который вызывает другой метод и возвращает локальную переменную
                sign_in = function(self)
                  self.num_players = self.num_players + 1
                  self:hello()
                  return self.num_players
                end
            }

В терминах ООП сейчас мы можем рассматривать внутренние переменные внутри переменной game как поля объекта, а внутренние функции – как методы объекта.

Примечание

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

Чтобы включить/отключить использование необъявленных глобальных переменных в вашем коде на языке Lua, используйте модуль Tarantool’а strict.

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

  • catch() (поймать) для расчета, когда был пойман покемон (помимо координат как игрока, так и покемона, этот метод будет использовать коэффициент вероятности, чтобы в пределах досягаемости игрока можно было поймать не каждого покемона);
  • respawn() (респаун) для добавления отсутствующих покемонов на карту, скажем, каждые 60 секунд (предположим, что испуганный покемон убегает, поэтому мы убираем покемона с карты при любой попытке поймать его и через некоторое время добавляем обратно на карту);
  • notify() (уведомить) для записи информации о пойманных покемонах (например, «Игрок 1 поймал покемона A»);
  • start() (начать) для инициализации игры (метод создаст спейсы в базе данных, создаст и скомпилирует avro-схемы, а также запустит метод respawn()).

Кроме того, было бы удобно завести методы для работы с хранилищем Tarantool’а. Например:

  • add_pokemon() (добавить покемона) для добавления покемона в базу данных и
  • map() (карта) для заполнения карты всеми покемонами, которые хранятся в Tarantool’е.

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

Настройка базы данных

Обсудим инициализацию игры. В методе start() нам нужно заполнить спейсы Tarantool’а данными о покемонах. Почему бы не хранить все игровые данные в памяти? Зачем нужна база данных? Ответ на это: персистентность. Без базы данных мы рискуем потерять данные при отключении электроэнергии, например. Но если мы храним данные в in-memory базе данных, Tarantool позаботится о том, чтобы обеспечить постоянное хранение данных при их изменении. Это дает дополнительное преимущество: быстрая загрузка в случае отказа. Умный алгоритм Tarantool’а быстро загружает все данные с диска в память при начале работы, так что подготовка к работе не займет много времени.

Мы будем использовать функции из встроенного модуля Tarantool’а box:

  • box.schema.create_space('pokemons') для создания спейса под названием pokemon (покемон), чтобы хранить информацию о покемонах (мы не создаем аналогичный спейс по игрокам, потому что планируем только отправлять и получать информацию об игроках с помощью вызовов API, так что нет необходимости хранить ее);
  • box.space.pokemons:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) для создания первичного HASH-индекса по ID покемона;
  • box.space.pokemons:create_index('status', {type = 'tree', parts = {2, 'str'}}) для создания вторичного TREE-индекса по статусу покемона.

Обратите внимание на аргумент parts = в спецификации индекса. ID покемона – это первое поле в кортеже Tarantool’а, потому что это первый элемент соответствующего типа Avro. То же относится к статусу покемона. В самом JSON-файле поля ID или статуса могут быть в любом положении на JSON-карте.

Реализация метода start() выглядит следующим образом:

-- создать игровой объект
 start = function(self)
     -- создать спейсы и индексы
     box.once('init', function()
         box.schema.create_space('pokemons')
         box.space.pokemons:create_index(
             "primary", {type = 'hash', parts = {1, 'unsigned'}}
         )
         box.space.pokemons:create_index(
             "status", {type = "tree", parts = {2, 'str'}}
         )
     end)

     -- создать модели
     local ok_m, pokemon = avro.create(schema.pokemon)
     local ok_p, player = avro.create(schema.player)
     if ok_m and ok_p then
         -- скомпилировать модели
         local ok_cm, compiled_pokemon = avro.compile(pokemon)
         local ok_cp, compiled_player = avro.compile(player)
         if ok_cm and ok_cp then
             -- начать игру
             <...>
         else
             log.error('Schema compilation failed')
         end
     else
         log.info('Schema creation failed')
     end
     return false
 end

ГИС

Теперь обсудим метод catch(), который является основным в логике нашей игры.

Здесь мы получаем координаты игрока и номер ID искомого покемона, а нужен нам ответ на вопрос, поймали ли игрок покемона (помните, что у каждого покемона есть шанс убежать).

Для начала проверим полученные данные об игроке по Avro-схеме. Также проверим, есть ли такой покемон в базе данных, и отображается ли он на карте (у покемона должен быть активный статус):

catch = function(self, pokemon_id, player)
                -- проверить данные игрока
                local ok, tuple = self.player_model.flatten(player)
                if not ok then
                    return false
                end
                -- получить данные покемона
                local p_tuple = box.space.pokemons:get(pokemon_id)
                if p_tuple == nil then
                    return false
                end
                local ok, pokemon = self.pokemon_model.unflatten(p_tuple)
                if not ok then
                    return false
                end
                if pokemon.status ~= self.state.ACTIVE then
                    return false
                end
                -- логика поимки будет дополняться
                <...>
            end

Далее вычисляем ответ: пойман или нет.

Чтобы работать с географическими координатами, используем модуль Tarantool’а gis.

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

Используем две географические переменные:

  • wgs84, что означает последнюю редакцию стандарта Мировой геодезической системы координат, WGS84. В целом, она представляет собой стандартную систему координат Земли и изображает Землю как эллипсоид.
  • nationalmap, что означает Государственный атлас США в равновеликой проекции (US National Atlas Equal Area). Это система спроецированных координат на основании WGS84. Она дает основу для проецирования мест и позволяет определить местоположение наших игроков и покемонов в метрах.

Обе системы указаны в Реестре геодезических параметров EPSG, где каждой системе присвоен уникальный номер. Мы назначим эти числа соответствующим переменным в нашем коде:

wgs84 = 4326,
            nationalmap = 2163,

Для игровой логики необходима еще одна переменная catch_distance, которая определяет, насколько близко игрок должен подойти к покемону, чтобы попытаться поймать его. Определим это расстояние в 100 метров.

catch_distance = 100,

Теперь можно рассчитать ответ. Необходимо спроецировать текущее местоположение как игрока (p_pos), так и покемона (m_pos) на карте, проверить, достаточно ли близко к покемону находится игрок (с помощью catch_distance), и рассчитать, поймал ли игрок покемона (здесь мы генерируем случайное значение, и покемон убегает, если случайное значение оказывается меньше, чем 100 минус случайная величина покемона):

-- спроецировать местоположение
 local m_pos = gis.Point(
     {pokemon.location.x, pokemon.location.y}, self.wgs84
 ):transform(self.nationalmap)
 local p_pos = gis.Point(
     {player.location.x, player.location.y}, self.wgs84
 ):transform(self.nationalmap)

 -- проверить условие близости игрока
 if p_pos:distance(m_pos) > self.catch_distance then
     return false
 end
 -- попытаться поймать покемона
 local caught = math.random(100) >= 100 - pokemon.chance
 if caught then
     -- обновить и сообщить об успехе
     box.space.pokemons:update(
         pokemon_id, {{'=', self.STATUS, self.state.CAUGHT}}
     )
     self:notify(player, pokemon)
 end
 return caught

Итератор с индексом

По сюжету игры все пойманные покемоны возвращаются на карту. Метод respawn() обеспечивает это для всех покемонов на карте каждые 60 секунд. Мы выполняем перебор покемонов по статусу с помощью функции Tarantool’а итератора с индексом index:pairs и сбрасываем статусы всех «пойманных» покемонов обратно на «активный» с помощью box.space.pokemons:update().

respawn = function(self)
     fiber.name('Respawn fiber')
     for _, tuple in box.space.pokemons.index.status:pairs(
            self.state.CAUGHT) do
         box.space.pokemons:update(
             tuple[self.ID],
             {{'=', self.STATUS, self.state.ACTIVE}}
         )
     end
  end

Для удобства введем именованные поля:

ID = 1, STATUS = 2,

Реализация метода start() полностью теперь выглядит так:

-- создать игровой объект
 start = function(self)
     -- создать спейсы и индексы
     box.once('init', function()
        box.schema.create_space('pokemons')
        box.space.pokemons:create_index(
            "primary", {type = 'hash', parts = {1, 'unsigned'}}
        )
        box.space.pokemons:create_index(
            "status", {type = "tree", parts = {2, 'str'}}
        )
     end)

     -- создать модели
     local ok_m, pokemon = avro.create(schema.pokemon)
     local ok_p, player = avro.create(schema.player)
     if ok_m and ok_p then
         -- скомпилировать модели
         local ok_cm, compiled_pokemon = avro.compile(pokemon)
         local ok_cp, compiled_player = avro.compile(player)
         if ok_cm and ok_cp then
             -- начать игру
             self.pokemon_model = compiled_pokemon
             self.player_model = compiled_player
             self.respawn()
             log.info('Started')
             return true
          else
             log.error('Schema compilation failed')
          end
     else
         log.info('Schema creation failed')
     end
     return false
 end

Файберы

Но подождите! Если мы запустим функцию self.respawn(), как показано выше, то она запустится только один раз, как и остальные методы. А нам необходимо запускать respawn() каждые 60 секунд. Tarantool заставляет логику приложения непрерывно работать в фоновом режиме с помощью файбера.

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

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

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

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

Производительность или управляемость не слишком важны в нашем случае. Запустим respawn() в файбере для непрерывной работы в фоновом режиме. Для этого необходимо изменить respawn():

respawn = function(self)
     -- назовем наш файбер;
     -- это выполнит чистый вывод в fiber.info()
     fiber.name('Respawn fiber')
     while true do
         for _, tuple in box.space.pokemons.index.status:pairs(
                 self.state.CAUGHT) do
             box.space.pokemons:update(
                 tuple[self.ID],
                 {{'=', self.STATUS, self.state.ACTIVE}}
             )
         end
         fiber.sleep(self.respawn_time)
     end
 end

и назвать его файбером в start():

start = function(self)
                -- создать спейсы и индексы
                    <...>
                -- создать модели
                    <...>
                -- скомпилировать модели
                    <...>
                -- начать игру
                   self.pokemon_model = compiled_pokemon
                   self.player_model = compiled_player
                   fiber.create(self.respawn, self)
                   log.info('Started')
                -- ошибки, если создание схемы или компиляция не работает
                   <...>
            end

Запись в журнал

В start() мы использовали еще одну полезную функцию – log.infо() из модуля log Tarantool’а . Эта функция также понадобится в notify() для добавления записи в файл журнала при каждой успешной поимке:

-- уведомление о событии
 notify = function(self, player, pokemon)
     log.info("Player '%s' caught '%s'", player.name, pokemon.name)
 end

Мы используем стандартные настройки журнала Tarantool’а, поэтому увидим вывод записей журнала в консоли, когда запустим приложение в режиме скрипта.

../../../../_images/aster.svg

Great! We’ve discussed all programming practices used in our Lua module (see pokemon.lua).

Now let’s prepare the test environment. As planned, we write a Lua application (see game.lua) to initialize Tarantool’s database module, initialize our game, call the game loop and simulate a couple of player requests.

Чтобы запустить микросервис, поместим модуль pokemon.lua и приложение game.lua в текущую директорию, установим все внешние модули и запустим экземпляр Tarantool’а с работают приложением game.lua (это пример для Ubuntu):

$ ls
            game.lua  pokemon.lua
            $ sudo apt-get install tarantool-gis
            $ sudo apt-get install tarantool-avro-schema
            $ tarantool game.lua

Tarantool запускает и инициализирует базу данных. Затем Tarantool выполняет демо-логику из game.lua: добавляет покемона под названием Пикачу (Pikachu) (шанс его поимки очень высок – 99,1), отображает текущую карту (на ней расположен один активный покемон, Пикачу) и обрабатывает запросы поимки от двух игроков. Player1 (Игрок 1) находится очень близко к одинокому покемону Пикачу, а Player2 (Игрок 2) находится очень далеко от него. Как предполагается, результаты поимки в таком выводе будут «true» для Player1 и «false» для Player2. Наконец, Tarantool отображает текущую карту, которая пуста, потому что Пикачу пойман и временно неактивен:

$ tarantool game.lua
 2017-01-09 20:19:24.605 [6282] main/101/game.lua C> version 1.7.3-43-gf5fa1e1
 2017-01-09 20:19:24.605 [6282] main/101/game.lua C> log level 5
 2017-01-09 20:19:24.605 [6282] main/101/game.lua I> mapping 1073741824 bytes for tuple arena...
 2017-01-09 20:19:24.609 [6282] main/101/game.lua I> initializing an empty data directory
 2017-01-09 20:19:24.634 [6282] snapshot/101/main I> saving snapshot `./00000000000000000000.snap.inprogress'
 2017-01-09 20:19:24.635 [6282] snapshot/101/main I> done
 2017-01-09 20:19:24.641 [6282] main/101/game.lua I> ready to accept requests
 2017-01-09 20:19:24.786 [6282] main/101/game.lua I> Started
 ---
 - {'id': 1, 'status': 'active', 'location': {'y': 2, 'x': 1}, 'name': 'Pikachu', 'chance': 99.1}
 ...

 2017-01-09 20:19:24.789 [6282] main/101/game.lua I> Player 'Player1' caught 'Pikachu'
 true
 false
 --- []
 ...

 2017-01-09 20:19:24.789 [6282] main C> entering the event loop

nginx

In the real life, this microservice would work over HTTP. Let’s add nginx web server to our environment and make a similar demo. But how do we make Tarantool methods callable via REST API? We use nginx with Tarantool nginx upstream module and create one more Lua script (app.lua) that exports three of our game methods – add_pokemon(), map() and catch() – as REST endpoints of the nginx upstream module:

local game = require('pokemon')
            box.cfg{listen=3301}
            game:start()

            -- функции add, map и catch по REST API
            function add(request, pokemon)
                return {
                    result=game:add_pokemon(pokemon)
                }
            end

            function map(request)
                return {
                    map=game:map()
                }
            end

            function catch(request, pid, player)
                local id = tonumber(pid)
                if id == nil then
                    return {result=false}
                end
                return {
                    result=game:catch(id, player)
                }
            end

An easy way to configure and launch nginx would be to create a Docker container based on a Docker image with nginx and the upstream module already installed (see http/Dockerfile). We take a standard nginx.conf, where we define an upstream with our Tarantool backend running (this is another Docker container, see details below):

upstream tnt {
                  server pserver:3301 max_fails=1 fail_timeout=60s;
                  keepalive 250000;
            }

и добавляем специальные параметры для Tarantool’а (см. описание в файле README модуля upstream):

server {
   server_name tnt_test;

   listen 80 default deferred reuseport so_keepalive=on backlog=65535;

   location = / {
       root /usr/local/nginx/html;
   }

   location /api {
     # ответы проверяют бесконечное время ожидания
     tnt_read_timeout 60m;
     if ( $request_method = GET ) {
        tnt_method "map";
     }
     tnt_http_rest_methods get;
     tnt_http_methods all;
     tnt_multireturn_skip_count 2;
     tnt_pure_result on;
     tnt_pass_http_request on parse_args;
     tnt_pass tnt;
   }
 }

Likewise, we put Tarantool server and all our game logic in a second Docker container based on the official Tarantool 1.9 image (see src/Dockerfile) and set the container’s default command to tarantool app.lua. This is the backend.

Неблокирующий ввод-вывод

To test the REST API, we create a new script (client.lua), which is similar to our game.lua application, but makes HTTP POST and GET requests rather than calling Lua functions:

local http = require('curl').http()
 local json = require('json')
 local URI = os.getenv('SERVER_URI')
 local fiber = require('fiber')

 local player1 = {
     name="Player1",
     id=1,
     location = {
         x=1.0001,
         y=2.0003
     }
 }
 local player2 = {
     name="Player2",
     id=2,
     location = {
         x=30.123,
         y=40.456
     }
 }

 local pokemon = {
     name="Pikachu",
     chance=99.1,
     id=1,
     status="active",
     location = {
         x=1,
         y=2
     }
 }

 function request(method, body, id)
     local resp = http:request(
         method, URI, body
     )
     if id ~= nil then
         print(string.format('Player %d result: %s',
             id, resp.body))
     else
         print(resp.body)
     end
 end

 local players = {}
 function catch(player)
     fiber.sleep(math.random(5))
     print('Catch pokemon by player ' .. tostring(player.id))
     request(
         'POST', '{"method": "catch",
         "params": [1, '..json.encode(player)..']}',
         tostring(player.id)
     )
     table.insert(players, player.id)
 end

 print('Create pokemon')
 request('POST', '{"method": "add",
     "params": ['..json.encode(pokemon)..']}')
 request('GET', '')

 fiber.create(catch, player1)
 fiber.create(catch, player2)

 -- подождать игроков
 while #players ~= 2 do
     fiber.sleep(0.001)
 end

 request('GET', '')
 os.exit()

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

Действительно, когда Player1 посылает первый REST-вызов, скрипт не блокируется. Файбер, выполняющий функцию catch() от Player1, посылает неблокирующий вызов в операционную систему и передает управление на следующий файбер, которым оказывается файбер от Player2. Файбер от Player2 делает то же самое. Когда получен сетевой ответ, файбер от Player1 активируется с помощью кооперативного планировщика Tarantool’а и возобновляет работу. Все модули Tarantool’а используют неблокирующий ввод-вывод и интегрированы с кооперативным планировщиком Tarantool’а. Разработчикам модулей Tarantool предоставляет API.

For our HTTP test, we create a third container based on the official Tarantool 1.9 image (see client/Dockerfile) and set the container’s default command to tarantool client.lua.

../../../../_images/aster.svg

Чтобы запустить тест локально, скачайте наш проект покемон из GitHub и вызовите:

$ docker-compose build
            $ docker-compose up

Docker Compose собирает и запускает все три контейнера: pserver (бэкенд Tarantool’а), phttp (nginx) и``pclient`` (демо-клиент). ВЫ можете увидеть все сообщения журнала из всех этих контейнеров в консоли. pclient выведет, что сделал HTTP-запрос на создание покемона, два запроса на поимку покемона, запросил карту (пустая, поскольку покемон пойман и временно неактивен) и завершил работу:

pclient_1  | Create pokemon
            <...>
            pclient_1  | {"result":true}
            pclient_1  | {"map":[{"id":1,"status":"active","location":{"y":2,"x":1},"name":"Pikachu","chance":99.100000}]}
            pclient_1  | Catch pokemon by player 2
            pclient_1  | Catch pokemon by player 1
            pclient_1  | Player 1 result: {"result":true}
            pclient_1  | Player 2 result: {"result":false}
            pclient_1  | {"map":[]}
            pokemon_pclient_1 exited with code 0

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

См. также справочник по модулям Tarantool’а и C API и не пропустите наши рекомендации по разработке на Lua.

Установка модуля

Модули на Lua и C от разработчиков Tarantool’а и сторонних разработчиков доступны здесь:

  • Репозиторий модулей Tarantool’а и
  • Репозитории deb/rpm Tarantool’а.

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

Для получения подробной информации см. README в репозитории tarantool/rocks.

Установка модуля из deb/rpm

Выполните следующие действия:

  1. Установите Tarantool в соответствии с рекомендациями на странице загрузки.

  2. Установите необходимый модуль. Найдите имя модуля на странице со сторонними библиотеками Tarantool’а и введите префикс «tarantool-» перед названием модуля во избежание неоднозначности:

    $ # для Ubuntu/Debian:
                $ sudo apt-get install tarantool-<module-name>
    
                $ # для RHEL/CentOS/Amazon:
                $ sudo yum install tarantool-<module-name>
    

    Например, чтобы установить модуль shard на Ubuntu, введите:

    $ sudo apt-get install tarantool-shard
    

Теперь можно:

  • загружать любой модуль с помощью

    tarantool> name = require('module-name')
    

    например:

    tarantool> shard = require('shard')
    
  • локально находить установленные модули с помощью package.path (Lua) или package.cpath (C):

    tarantool> package.path
                ---
                - ./?.lua;./?/init.lua; /usr/local/share/tarantool/?.lua;/usr/local/share/
                tarantool/?/init.lua;/usr/share/tarantool/?.lua;/usr/share/tarantool/?/ini
                t.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/
                usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;
                ...
    
                tarantool> package.cpath
                ---
                - ./?.so;/usr/local/lib/x86_64-linux- gnu/tarantool/?.so;/usr/lib/x86_64-li
                nux- gnu/tarantool/?.so;/usr/local/lib/tarantool/?.so;/usr/local/lib/x86_64
                -linux-gnu/lua/5.1/?.so;/usr/lib/x86_64-linux- gnu/lua/5.1/?.so;/usr/local/
                lib/lua/5.1/?.so;
                ...
    

    Примечание

    Знаки вопроса стоят вместо имени модуля, которое было указано ранее при вызове require('module-name').

Добавление собственного модуля

Мы уже обсуждали, как создать простой модуль на языке Lua для локального использования.Теперь давайте обсудим, как создать модуль более продвинутого уровня для Tarantool’а, а затем разместить его на странице модулей Tarantool’а <http://tarantool.org/rocks.html>`_ и включить его в официальные образы Tarantool’а для Docker.

Чтобы помочь разработчикам, мы создали modulekit, набор шаблонов для создания Tarantool-модулей на Lua и C.

Примечание

Чтобы использовать modulekit, необходимо предварительно установить пакет tarantool-dev. Например, в Ubuntu выполните команду:

$ sudo apt-get install tarantool-dev

Добавление собственного модуля на Lua

Подробную информацию и примеры см. в README в ветке «luakit» репозитория tarantool/modulekit.

Добавление собственного модуля на C

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

Подробную информацию и примеры см. в README в ветке «ckit» репозитория tarantool/modulekit.

Примечание

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

Перезагрузка модуля

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

Перезагрузка модуля на Lua

Ниже представлен пример, который иллюстрирует наиболее типичный случай – «обновление и перезагрузка».

Примечание

В этом примере используются рекомендованные методики администрирования на основании файлов экземпляров и утилиты tarantoolctl.

  1. Обновите файлы приложения.

    Например, модуль в /usr/share/tarantool/app.lua:

    local function start()
       -- начальная версия
       box.once("myapp:v1.0", function()
         box.schema.space.create("somedata")
         box.space.somedata:create_index("primary")
         ...
       end)
    
       -- код миграции с 1.0 на 1.1
       box.once("myapp:v1.1", function()
         box.space.somedata.index.primary:alter(...)
         ...
       end)
    
       -- код миграции с 1.1 на 1.2
       box.once("myapp:v1.2", function()
         box.space.somedata.index.primary:alter(...)
         box.space.somedata:insert(...)
         ...
       end)
     end
    
     -- запустить файберы в фоновом режиме, если необходимо
    
     local function stop()
       -- остановить все файберы, работающие в фоновом режиме, и очистить ресурсы
     end
    
     local function api_for_call(xxx)
       -- do some business
     end
    
     return {
       start = start,
       stop = stop,
       api_for_call = api_for_call
     }
    
  2. Обновить файл экземпляра.

    Например, /etc/tarantool/instances.enabled/my_app.lua:

    #!/usr/bin/env tarantool
                --
                -- hot code reload example
                --
    
                box.cfg{listen = 3302}})
    
                -- ATTENTION: unload it all properly!
                local app = package.loaded['app']
                if app ~= nil then
                  -- stop the old application version
                  app.stop()
                  -- unload the application
                  package.loaded['app'] = nil
                  -- unload all dependencies
                  package.loaded['somedep'] = nil
                end
    
                -- load the application
                log.info('require app')
                app = require('app')
    
                -- start the application
                app.s{some app options controlled by sysadmins}mins})
    

    Самое главное – правильно разгрузить приложение и его зависимости.

  3. Вручную перезагрузите файл приложения.

    Например, используя tarantoolctl:

    $ tarantoolctl eval my_app /etc/tarantool/instances.enabled/my_app.lua
    

Перезагрузка модуля на С

После компиляции новой версии модуля на C (библиотека общего пользования *.so), вызовите функцию box.schema.func.reload(„module-name“) из Lua-скрипта для перезагрузки модуля.

Разработка с IDE

Для разработки и отладки Lua-приложений для Tarantool’а можно использовать IntelliJ IDEA в качестве интегрированной среды разработки (IDE).

  1. Загрузите и установите IDE с официального сайта.

    JetBrains предоставляет специализированные версии для разных языков программирования: IntelliJ IDEA (Java), PHPStorm (PHP), PyCharm (Python), RubyMine (Ruby), CLion (C/C++), WebStorm (Web) и другие. Поэтому загрузите версию, которая подходит предпочитаемому языку.

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

  2. Настройте IDE:

    1. Запустите IntelliJ IDEA.

    2. Нажмите кнопку Configure и выберите Plugins.

      ../../../../_images/ide_1.png
    3. Нажмите Browse repositories.

      ../../../../_images/ide_2.png
    4. Установите плагин EmmyLua.

      Примечание

      Не путайте с плагином Lua, у которого меньше возможностей, чем у EmmyLua.

      ../../../../_images/ide_3.png
    5. Перезапустите IntelliJ IDEA.

    6. Нажмите Configure, выберите Project Defaults, а затем Run Configurations.

      ../../../../_images/ide_4.png
    7. Найдите Lua Application в боковой панели слева.

    8. В Program введите путь к установленному бинарному файлу tarantool.

      По умолчанию, это tarantool или /usr/bin/tarantool на большинстве платформ.

      Если вы установили tarantool из источников в другую директорию, укажите здесь правильный путь.

      ../../../../_images/ide_5.png

      Теперь IntelliJ IDEA можно использовать с Tarantool’ом.

  3. Создайте новый проект на Lua.

    ../../../../_images/ide_6.png
  4. Добавьте новый Lua-файл, например, init.lua.

    ../../../../_images/ide_7.png
  5. Разработайте код, сохраните файл.

  6. Чтобы запустить приложение, нажмите Run -> Run в основном меню и выберите исходный файл из списка.

    ../../../../_images/ide_8.png

    Или нажмите Run -> Debug для начала отладки.

    Примечание

    Чтобы использовать Lua-отладчик, обновите Tarantool до версии 1.7.5-29-gbb6170e4b или более поздней версии.

    ../../../../_images/ide_9.png

Примеры и рекомендации по разработке

Ниже представлены дополнения в виде Lua-программ для часто встречающихся или сложных случаев.

Любую из этих программ можно выполнить, скопировав код в .lua-файл, а затем выполнив в командной строке chmod +x ./имя-программы.lua и :samp :./{имя-программы}.lua.

Первая строка – это шебанг:

#!/usr/bin/env tarantool

Он запускает сервер приложений Tarantool’а на языке Lua, который должен быть в пути выполнения.

В этом разделе собраны следующие рецепты:

Можно использовать свободно.

hello_world.lua

Стандартный пример простой программы.

#!/usr/bin/env tarantool

 print('Hello, World!')

console_start.lua

Для инициализации базы данных (создания спейсов) используйте box.once(), если сервер запускается впервые. Затем используйте console.start(), чтобы запустить интерактивный режим.

#!/usr/bin/env tarantool

 -- Настроить базу данных
 box.cfg {
     listen = 3313
 }

 box.once("bootstrap", function()
     box.schema.space.create('tweedledum')
     box.space.tweedledum:create_index('primary',
         { type = 'TREE', parts = {1, 'unsigned'}})
 end)

 require('console').start()

fio_read.lua

Используйте Модуль fio, чтобы открыть, прочитать и закрыть файл.

#!/usr/bin/env tarantool

 local fio = require('fio')
 local errno = require('errno')
 local f = fio.open('/tmp/xxxx.txt', {'O_RDONLY' })
 if not f then
     error("Failed to open file: "..errno.strerror())
 end
 local data = f:read(4096)
 f:close()
 print(data)

fio_write.lua

Используйте Модуль fio, чтобы открыть, записать данные и закрыть файл.

#!/usr/bin/env tarantool

 local fio = require('fio')
 local errno = require('errno')
 local f = fio.open('/tmp/xxxx.txt', {'O_CREAT', 'O_WRONLY', 'O_APPEND'},
     tonumber('0666', 8))
 if not f then
     error("Failed to open file: "..errno.strerror())
 end
 f:write("Hello\n");
 f:close()

ffi_printf.lua

Используйте Библиотеку LuaJIT FFI, чтобы вызвать встроенную в C функцию: printf(). (Чтобы лучше понимать FFI, см. Учебное пособие по FFI.)

#!/usr/bin/env tarantool

 local ffi = require('ffi')
 ffi.cdef[[
     int printf(const char *format, ...);
 ]]

 ffi.C.printf("Hello, %s\n", os.getenv("USER"));

ffi_gettimeofday.lua

Используйте Библиотеку LuaJIT FFI, чтобы вызвать встроенную в C функцию: gettimeofday(). Она позволяет получить значение времени с точностью в миллисекундах, в отличие от функции времени в Tarantool’е Модуль clock.

#!/usr/bin/env tarantool

 local ffi = require('ffi')
 ffi.cdef[[
     typedef long time_t;
     typedef struct timeval {
     time_t tv_sec;
     time_t tv_usec;
 } timeval;
     int gettimeofday(struct timeval *t, void *tzp);
 ]]

 local timeval_buf = ffi.new("timeval")
 local now = function()
     ffi.C.gettimeofday(timeval_buf, nil)
     return tonumber(timeval_buf.tv_sec * 1000 + (timeval_buf.tv_usec / 1000))
 end

ffi_zlib.lua

Используйте Библиотеку LuaJIT FFI, чтобы вызвать библиотечную функцию в C. (Чтобы лучше понимать FFI, см. Учебное пособие по FFI.)

#!/usr/bin/env tarantool

 local ffi = require("ffi")
 ffi.cdef[[
     unsigned long compressBound(unsigned long sourceLen);
     int compress2(uint8_t *dest, unsigned long *destLen,
     const uint8_t *source, unsigned long sourceLen, int level);
     int uncompress(uint8_t *dest, unsigned long *destLen,
     const uint8_t *source, unsigned long sourceLen);
 ]]
 local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")

 -- Надстройка Lua для функции compress2()
 local function compress(txt)
     local n = zlib.compressBound(#txt)
     local buf = ffi.new("uint8_t[?]", n)
     local buflen = ffi.new("unsigned long[1]", n)
     local res = zlib.compress2(buf, buflen, txt, #txt, 9)
     assert(res == 0)
     return ffi.string(buf, buflen[0])
 end

 -- Надстройка Lua для функции uncompress
 local function uncompress(comp, n)
     local buf = ffi.new("uint8_t[?]", n)
     local buflen = ffi.new("unsigned long[1]", n)
     local res = zlib.uncompress(buf, buflen, comp, #comp)
     assert(res == 0)
     return ffi.string(buf, buflen[0])
 end

 -- Простой код тестов
 local txt = string.rep("abcd", 1000)
 print("Uncompressed size: ", #txt)
 local c = compress(txt)
 print("Compressed size: ", #c)
 local txt2 = uncompress(c, #txt)
 assert(txt2 == txt)

ffi_meta.lua

Используйте Библиотеку LuaJIT FFI, чтобы получить доступ к объекту в C с помощью метаметода (метод, который определен метатаблицей).

#!/usr/bin/env tarantool

 local ffi = require("ffi")
 ffi.cdef[[
 typedef struct { double x, y; } point_t;
 ]]

 local point
 local mt = {
   __add = function(a, b) return point(a.x+b.x, a.y+b.y) end,
   __len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,
   __index = {
     area = function(a) return a.x*a.x + a.y*a.y end,
   },
 }
 point = ffi.metatype("point_t", mt)

 local a = point(3, 4)
 print(a.x, a.y)  --> 3  4
 print(#a)        --> 5
 print(a:area())  --> 25
 local b = a + point(0.5, 8)
 print(#b)        --> 12.5

count_array.lua

Используйте оператор „#“, чтобы получить количество элементов в Lua-таблице типа массива. У этой операции сложность O(log(N)).

#!/usr/bin/env tarantool

 array = { 1, 2, 3}
 print(#array)

count_array_with_nils.lua

Отсутствующие элементы в массивах, которые Lua рассматривает как nil, заставляют простой оператор „#“ выдавать неправильные результаты. Команда «print(#t)» выведет «4», команда «print(counter)» выведет «3», а команда «print(max)» – «10». Другие табличные функции, такие как table.sort(), также сработают неправильно при наличии значений nils.

#!/usr/bin/env tarantool

 local t = {}
 t[1] = 1
 t[4] = 4
 t[10] = 10
 print(#t)
 local counter = 0
 for k,v in pairs(t) do counter = counter + 1 end
 print(counter)
 local max = 0
 for k,v in pairs(t) do if k > max then max = k end end
 print(max)

count_array_with_nulls.lua

Используйте явные значения``NULL``, чтобы избежать проблем, вызванных nil в Lua == поведение с пропущенными значениями. Хотя json.NULL == nil является true, все команды вывода в данной программе выведут правильное значение: 10.

#!/usr/bin/env tarantool

 local json = require('json')
 local t = {}
 t[1] = 1; t[2] = json.NULL; t[3]= json.NULL;
 t[4] = 4; t[5] = json.NULL; t[6]= json.NULL;
 t[6] = 4; t[7] = json.NULL; t[8]= json.NULL;
 t[9] = json.NULL
 t[10] = 10
 print(#t)
 local counter = 0
 for k,v in pairs(t) do counter = counter + 1 end
 print(counter)
 local max = 0
 for k,v in pairs(t) do if k > max then max = k end end
 print(max)

count_map.lua

Программа используется для получения количества элементов в таблице типа ассоциативного массива.

#!/usr/bin/env tarantool

 local map = { a = 10, b = 15, c = 20 }
 local size = 0
 for _ in pairs(map) do size = size + 1; end
 print(size)

swap.lua

Программа использует особенность Lua менять местами две переменные без необходимости использования третьей переменной.

#!/usr/bin/env tarantool

 local x = 1
 local y = 2
 x, y = y, x
 print(x, y)

class.lua

Используется для создания класса, метатаблицы для класса, экземпляра класса. Другой пример можно найти в http://lua-users.org/wiki/LuaClassesWithMetatable.

#!/usr/bin/env tarantool

 -- определить объекты класса
 local myclass_somemethod = function(self)
     print('test 1', self.data)
 end

 local myclass_someothermethod = function(self)
     print('test 2', self.data)
 end

 local myclass_tostring = function(self)
     return 'MyClass <'..self.data..'>'
 end

 local myclass_mt = {
     __tostring = myclass_tostring;
     __index = {
         somemethod = myclass_somemethod;
         someothermethod = myclass_someothermethod;
     }
 }

 -- создать новый объект своего класса myclass
 local object = setmetatable({ data = 'data'}, myclass_mt)
 print(object:somemethod())
 print(object.data)

garbage.lua

Activate the Lua garbage collector with the collectgarbage function.

#!/usr/bin/env tarantool

 collectgarbage('collect')

fiber_producer_and_consumer.lua

Запустите один файбер для производителя и один файбер для потребителя. Используйте fiber.channel() для обмена данных и синхронизации. Можно настроить ширину канала (ch_size в программном коде) для управления количеством одновременных задач к обработке.

#!/usr/bin/env tarantool

 local fiber = require('fiber')
 local function consumer_loop(ch, i)
     -- инициализировать потребитель синхронно или выдать ошибку()
     fiber.sleep(0) -- позволить fiber.create() продолжать
     while true do
         local data = ch:get()
         if data == nil then
             break
         end
         print('consumed', i, data)
         fiber.sleep(math.random()) -- моделировать работу
     end
 end

 local function producer_loop(ch, i)
     -- инициализировать потребитель синхронно или выдать ошибку()
     fiber.sleep(0) -- allow fiber.create() to continue
     while true do
         local data = math.random()
         ch:put(data)
         print('produced', i, data)
     end
 end

 local function start()
     local consumer_n = 5
     local producer_n = 3

     -- создать канал
     local ch_size = math.max(consumer_n, producer_n)
     local ch = fiber.channel(ch_size)

     -- запустить потребители
     for i=1, consumer_n,1 do
         fiber.create(consumer_loop, ch, i)
     end

     -- запустить производители
     for i=1, producer_n,1 do
         fiber.create(producer_loop, ch, i)
     end
 end

 start()
 print('started')

socket_tcpconnect.lua

Используйте socket.tcp_connect() для подключения к удаленному серверу по TCP. Можно отобразить информацию о подключении и результат запроса GET.

#!/usr/bin/env tarantool

 local s = require('socket').tcp_connect('google.com', 80)
 print(s:peer().host)
 print(s:peer().family)
 print(s:peer().type)
 print(s:peer().protocol)
 print(s:peer().port)
 print(s:write("GET / HTTP/1.0\r\n\r\n"))
 print(s:read('\r\n'))
 print(s:read('\r\n'))

socket_tcp_echo.lua

Используйте socket.tcp_connect() для настройки простого TCP-сервера путем создания функции, которая обрабатывает запросы и отражает их, а затем передачи функции на socket.tcp_server(). Данная программа была протестирована на 100 000 клиентов, каждый из которых получил отдельный файбер.

#!/usr/bin/env tarantool

 local function handler(s, peer)
     s:write("Welcome to test server, " .. peer.host .."\n")
     while true do
         local line = s:read('\n')
         if line == nil then
             break -- ошибка или eof
         end
         if not s:write("pong: "..line) then
             break -- ошибка или eof
         end
     end
 end

 local server, addr = require('socket').tcp_server('localhost', 3311, handler)

getaddrinfo.lua

Используйте socket.getaddrinfo(), чтобы провести неблокирующее разрешение имен DNS, получая как AF_INET6, так и AF_INET информацию для „google.com“. Данная техника не всегда необходима для TCP-соединений, поскольку socket.tcp_connect() выполняет socket.getaddrinfo с точки зрения внутреннего устройства до попытки соединения с первым доступным адресом.

#!/usr/bin/env tarantool

 local s = require('socket').getaddrinfo('google.com', 'http', { type = 'SOCK_STREAM' })
 print('host=',s[1].host)
 print('family=',s[1].family)
 print('type=',s[1].type)
 print('protocol=',s[1].protocol)
 print('port=',s[1].port)
 print('host=',s[2].host)
 print('family=',s[2].family)
 print('type=',s[2].type)
 print('protocol=',s[2].protocol)
 print('port=',s[2].port)

socket_udp_echo.lua

В данный момент в Tarantool нет функции udp_server, поэтому socket_udp_echo.lua – более сложная программа, чем socket_tcp_echo.lua. Ее можно реализовать с помощью сокетов и файберов.

#!/usr/bin/env tarantool

 local socket = require('socket')
 local errno = require('errno')
 local fiber = require('fiber')

 local function udp_server_loop(s, handler)
     fiber.name("udp_server")
     while true do
         -- попытка прочитать сначала датаграмму
         local msg, peer = s:recvfrom()
         if msg == "" then
             -- сокет был закрыт с помощью s:close()
             break
         elseif msg ~= nil then
             -- получена новая датаграмма
             handler(s, peer, msg)
         else
             if s:errno() == errno.EAGAIN or s:errno() == errno.EINTR then
                 -- сокет не готов
                 s:readable() -- передача управления, epoll сообщит, когда будут новые данные
             else
                 -- ошибка сокета
                 local msg = s:error()
                 s:close() -- сохранить ресурсы и не ждать сборку мусора
                 error("Socket error: " .. msg)
             end
         end
     end
 end

 local function udp_server(host, port, handler)
     local s = socket('AF_INET', 'SOCK_DGRAM', 0)
     if not s then
         return nil -- проверить номер ошибки errno:strerror()
     end
     if not s:bind(host, port) then
         local e = s:errno() -- сохранить номер ошибки errno
         s:close()
         errno(e) -- восстановить номер ошибки errno
         return nil -- проверить номер ошибки errno:strerror()
     end

     fiber.create(udp_server_loop, s, handler) -- запустить новый файбер в фоновом режиме
     return s
 end

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

local function handler(s, peer, msg)
     -- Необязательно ждать, пока сокет будет готов отправлять UDP
     -- s:writable()
     s:sendto(peer.host, peer.port, "Pong: " .. msg)
 end

 local server = udp_server('127.0.0.1', 3548, handler)
 if not server then
     error('Failed to bind: ' .. errno.strerror())
 end

 print('Started')

 require('console').start()

http_get.lua

Используйте Модуль HTTP для получения данных по HTTP.

#!/usr/bin/env tarantool

 local http_client = require('http.client')
 local json = require('json')
 local r = http_client.get('http://api.openweathermap.org/data/2.5/weather?q=Oakland,us')
 if r.status ~= 200 then
     print('Failed to get weather forecast ', r.reason)
     return
 end
 local data = json.decode(r.body)
 print('Oakland wind speed: ', data.wind.speed)

http_send.lua

Используйте Модуль HTTP для отправки данных по HTTP.

#!/usr/bin/env tarantool

            local http_client = require('http.client')
            local json = require('json')
            local data = json.encode({ Key = 'Value'})
            local headers = { Token = 'xxxx', ['X-Secret-Value'] = 42 }
            local r = http_client.post('http://localhost:8081', data, { headers = headers})
            if r.status == 200 then
                print 'Success'
            end

http_server.lua

Используйте сторонний модуль http (который необходимо предварительно установить), чтобы превратить Tarantool в веб-сервер.

#!/usr/bin/env tarantool

            local function handler(self)
                return self:render{ json = { ['Your-IP-Is'] = self.peer.host } }
            end

            local server = require('http.server').new(nil, 8080) -- анализировать связь с *:8080
            server:route({ path = '/' }, handler)
            server:start()
            -- подключиться к localhost:8080 и читать JSON

http_generate_html.lua

Используйте сторонний модуль http (который необходимо предварительно установить) для создания HTML-страниц из шаблонов. В модуле http достаточно простой движок шаблонов, который позволяет выполнять регулярный код на Lua в текстовых блоках (как в PHP). Таким образом, нет необходимости в изучении новых языков, чтобы написать шаблоны.

#!/usr/bin/env tarantool

 local function handler(self)
 local fruits = { 'Apple', 'Orange', 'Grapefruit', 'Banana'}
     return self:render{ fruits = fruits }
 end

 local server = require('http.server').new(nil, 8080) -- nil означает '*'
 server:route({ path = '/', file = 'index.html.lua' }, handler)
 server:start()

HTML-файл для этого сервера, включая Lua, может выглядеть следующим образом (он выведет «1 Apple | 2 Orange | 3 Grapefruit | 4 Banana»).

<html>
 <body>
     <table border="1">
         % for i,v in pairs(fruits) do
         <tr>
             <td><%= i %></td>
             <td><%= v %></td>
         </tr>
         % end
     </table>
 </body>
 </html>

Администрирование серверной части

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

Здесь мы показываем, как администрировать экземпляры Tarantool’а с помощью любой из следующих утилит:

  • встроенные утилиты systemd или
  • tarantoolctl, утилита, поставляемая и устанавливаемая вместе с дистрибутивом Tarantool’а.

Примечание

  • В отличие от остальной части руководства, в этой главе мы используем общесистемные пути.
  • Здесь мы приводим примеры консольного вывода для Fedora.

Эта глава включает в себя следующие разделы:

Настройка экземпляров Tarantool’а

Для каждого экземпляра Tarantool’а понадобится два файла:

  • [Необязательный] Файл приложения, содержащий логику данного экземпляра. Поместите его в папку /usr/share/tarantool/.

    Например, /usr/share/tarantool/my_app.lua (здесь мы реализуем его как Lua-модуль, который запускает базу данных и экспортирует функцию start() для API -вызовов):

    local function start()
          box.schema.space.create("somedata")
          box.space.somedata:create_index("primary")
          <...>
      end
    
      return {
        start = start;
      }
    
  • Файл экземпляра, содержащий логику и параметры инициализации данного экземпляра. Поместите этот файл или символьную ссылку на него в директорию экземпляра (см. параметр instance_dir в конфигурационном файле tarantoolctl).

    Например, /etc/tarantool/instances.enabled/my_app.lua (здесь мы загружаем модуль my_app.lua и вызываем из него функцию start()):

    #!/usr/bin/env tarantool
    
      box.cfg {
          listen = 3301;
      }
    
      -- загрузить модуль my_app и вызвать функцию start()
      -- некоторые опции приложения под контролем сисадминов
      local m = require('my_app').start({...})
    

Файл экземпляра

После столь краткого предисловия может возникнуть вопрос: что из себя представляет файл экземпляра, для чего он нужен и как tarantoolctl использует его? Если Tarantool – это сервер приложений, так почему бы не запускать хранящееся в /usr/share/tarantool приложение напрямую?

Типичное приложение для Tarantool – это не скрипт, а демон, запущенный в фоновом режиме и обрабатывающий запросы, которые, как правило, посылаются через TCP/IP-сокет. Необходимо запускать этот демон со стартом операционной системы и управлять им с помощью стандартных средств операционной системы для управления сервисами – таких как systemd или init.d. С этой целью и были созданы файлы экземпляра.

Файлов экземпляра может быть больше одного. Например, одно и то же приложение в /usr/share/tarantool может быть запущено на нескольких экземплярах Tarantool’а, у каждого из которых есть свой файл экземпляра. Или в /usr/share/tarantool может быть несколько приложений, и на каждое из них будет опять же приходиться свой файл экземпляра.

Обычно файл экземпляра создает системный администратор, а файл приложения предоставляет разработчик в Lua-модуле или rpm/deb-пакете.

По своему устройству файл экземпляра ничем не отличается от Lua-приложения. Однако с его помощью должна настраиваться база данных, поэтому в нем должен содержаться вызов box.cfg{}, потому что это единственный способ превратить Tarantool-скрипт в фоновый процесс, а tarantoolctl – это инструмент для управления фоновыми процессами. За исключением этого вызова, файл экземпляра может содержать произвольный код на Lua и, теоретически, даже всю бизнес-логику приложения. Однако мы не рекомендуем хранить весь код в файле экземпляра, потому что это приводит как к замусориванию самого файла, так и к ненужному копированию кода при необходимости запустить несколько экземпляров приложения.

Конфигурационный файл tarantoolctl

While instance files contain instance configuration, the tarantoolctl configuration file contains the configuration that tarantoolctl uses to override instance configuration. In other words, it contains system-wide configuration defaults. If tarantoolctl fails to find this file with the method described in section Starting/stopping an instance, it uses default settings.

Most of the parameters are similar to those used by box.cfg{}. Here are the default settings (possibly installed in /etc/default/tarantool or /etc/sysconfig/tarantool as part of Tarantool distribution – see OS-specific default paths in Notes for operating systems):

default_cfg = {
      pid_file  = "/var/run/tarantool",
      wal_dir   = "/var/lib/tarantool",
      memtx_dir = "/var/lib/tarantool",
      vinyl_dir = "/var/lib/tarantool",
      log       = "/var/log/tarantool",
      username  = "tarantool",
  }
  instance_dir = "/etc/tarantool/instances.enabled"

где:

  • pid_file
    Директория, где хранятся pid-файл и socket-файл; tarantoolctl добавляет “/имя_экземпляра” к имени директории.
  • wal_dir
    Директория, где хранятся .xlog-файлы; tarantoolctl добавляет “/имя_экземпляра” к имени директории.
  • memtx_dir
    Директория, где хранятся .snap-файлы; tarantoolctl добавляет “/имя_экземпляра” к имени директории.
  • vinyl_dir
    Директория, где хранятся vinyl-файлы; tarantoolctl добавляет “/имя_экземпляра” к имени директории.
  • log
    Директория, где хранятся файлы журнала с сообщениями от Tarantool-приложения; tarantoolctl добавляет “/имя_экземпляра” к имени директории.
  • username
    Пользователь, запускающий экземпляр Tarantool’а. Это пользователь операционной системы, а не Tarantool-клиента. Став демоном, Tarantool сменит своего пользователя на указанного.
  • instance_dir
    Директория, где хранятся все файлы экземпляра для данного компьютера. Поместите сюда файлы экземпляра или создайте символьные ссылки на них.

    Директория с экземплярами, которая используется по умолчанию, зависит от параметра WITH_SYSVINIT сборки Tarantool’а: когда его значение «ON», то /etc/tarantool/instances.enabled, в противном случае («OFF» или значение не установлено), то /etc/tarantool/instances.available. Последний случай характерен для сборок Tarantool’а для дистрибутивов Linux с systemd.

    Для проверки параметров сборки выполните команду tarantool --version.

As a full-featured example, you can take example.lua script that ships with Tarantool and defines all configuration options.

Запуск/остановка экземпляра

Lua-приложение выполняется Tarantool’ом, тогда как файл экземпляра выполняется Tarantool-скриптом tarantoolctl.

Вот что делает tarantoolctl при вводе следующей команды:

$ tarantoolctl start <имя_экземпляра>
  1. Считывает и разбирает аргументы командной строки. В нашем случае последний аргумент содержит имя экземпляра.

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

    When tarantool is invoked by root, it looks for a configuration file in /etc/default/tarantool. When tarantool is invoked by a local (non-root) user, it looks for a configuration file first in the current directory ($PWD/.tarantoolctl), and then in the current user’s home directory ($HOME/.config/tarantool/tarantool). If no configuration file is found there, or in the /usr/local/etc/default/tarantool file, then tarantoolctl falls back to built-in defaults.

  3. Look up the instance file in the instance directory, for example /etc/tarantool/instances.enabled. To build the instance file path, tarantoolctl takes the instance name, prepends the instance directory and appends «.lua» extension to the instance file.

  4. Переопределяет функцию box.cfg{}, чтобы предобработать ее параметры и сделать так, чтобы пути к экземплярам указывали на пути, прописанные в конфигурационном файле tarantoolctl. Например, если в конфигурационном файле указано, что рабочей директорией экземпляра является /var/tarantool, то новая реализация box.cfg{} сделает так, чтобы параметр work_dir в box.cfg{} имел значение /var/tarantool/<имя_экземпляра>, независимо от того, какой путь указан в самом файле экземпляра.

  5. Создает так называемый «файл для управления экземпляром». Это Unix-сокет с прикрепленной к нему Lua-консолью. В дальнейшем tarantoolctl использует этот файл для получения состояния экземпляра, отправки команд и т.д.

  6. Set the TARANTOOLCTL environment variable to „true“. This allows the user to know that the instance was started by tarantoolctl.

  7. Наконец, использует Lua-команду dofile для выполнения файла экземпляра.

При запуске экземпляра с помощью инструментария systemd указанным ниже способом (имя экземпляра – my_app):

$ systemctl start tarantool@my_app
  $ ps axuf|grep exampl[e]
  taranto+  5350  1.3  0.3 1448872 7736 ?        Ssl  20:05   0:28 tarantool my_app.lua <running>

…на самом деле вызывается tarantoolctl – так же, как и в случае tarantoolctl start my_app.

Для проверки файла экземпляра на наличие синтаксических ошибок перед запуском экземпляра my_app используйте команду:

$ tarantoolctl check my_app

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

$ systemctl enable tarantool@my_app

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

$ tarantoolctl stop my_app
  $ # - ИЛИ -
  $ systemctl stop tarantool@my_app

Для перезапуска (т.е. остановки и запуска) работающего экземпляра my_app используйте команду:

$ tarantoolctl restart my_app
 $ # - ИЛИ -
 $ systemctl restart tarantool@my_app

Локальный запуск Tarantool

Иногда бывает необходимо запустить Tarantool локально – например, для тестирования. Давайте настроим локальный экземпляр, запустим его и будем мониторить с помощью tarantoolctl.

Сперва создадим директорию-песочницу по следующему пути:

$ mkdir ~/tarantool_test

…и поместим конфигурационный файл с параметрами tarantoolctl по умолчанию в $HOME/.config/tarantool/tarantool. Содержимое файла будет таким:

default_cfg = {
     pid_file  = "/home/user/tarantool_test/my_app.pid",
     wal_dir   = "/home/user/tarantool_test",
     snap_dir  = "/home/user/tarantool_test",
     vinyl_dir = "/home/user/tarantool_test",
     log       = "/home/user/tarantool_test/log",
 }
 instance_dir = "/home/user/tarantool_test"

Примечание

  • Указывайте полный путь к домашней директории пользователя вместо «~/».
  • Опустите параметр username. Обычно, когда запуск производит локальный пользователь, у tarantoolctl нет разрешения на смену текущего пользователя. Экземпляр будет работать с пользователем „admin“.

Далее создадим файл экземпляра ~/tarantool_test/my_app.lua. Содержимое файла будет таким:

box.cfg{listen = 3301}
 box.schema.user.passwd('Gx5!')
 box.schema.user.grant('guest','read,write,execute','universe')
 fiber = require('fiber')
 box.schema.space.create('tester')
 box.space.tester:create_index('primary',{})
 i = 0
 while 0 == 0 do
     fiber.sleep(5)
     i = i + 1
     print('insert ' .. i)
     box.space.tester:insert{i, 'my_app tuple'}
 end

Проверим наш файл экземпляра, сперва запустив его без tarantoolctl:

$ cd ~/tarantool_test
 $ tarantool my_app.lua
 2017-04-06 10:42:15.762 [54085] main/101/my_app.lua C> version 1.7.3-489-gd86e36d5b
 2017-04-06 10:42:15.763 [54085] main/101/my_app.lua C> log level 5
 2017-04-06 10:42:15.764 [54085] main/101/my_app.lua I> mapping 268435456 bytes for tuple arena...
 2017-04-06 10:42:15.774 [54085] iproto/101/main I> binary: bound to [::]:3301
 2017-04-06 10:42:15.774 [54085] main/101/my_app.lua I> initializing an empty data directory
 2017-04-06 10:42:15.789 [54085] snapshot/101/main I> saving snapshot `./00000000000000000000.snap.inprogress'
 2017-04-06 10:42:15.790 [54085] snapshot/101/main I> done
 2017-04-06 10:42:15.791 [54085] main/101/my_app.lua I> vinyl checkpoint done
 2017-04-06 10:42:15.791 [54085] main/101/my_app.lua I> ready to accept requests
 insert 1
 insert 2
 insert 3
 <...>

Запустим экземпляр Tarantool’а с помощью tarantoolctl:

$ tarantoolctl start my_app

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

$ ls -l ~/tarantool_test/my_app

В консоли должны появиться .snap-файл и .xlog-файл. Затем выполним следующую команду:

$ less ~/tarantool_test/log/my_app.log

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

$ tarantoolctl enter my_app
 tarantool> box.cfg{}
 tarantool> console = require('console')
 tarantool> console.connect('localhost:3301')
 tarantool> box.space.tester:select({0}, {iterator = 'GE'})

В консоли должны появиться несколько кортежей, которые создало приложение my_app.

Теперь остановим приложение my_app. Корректный способ остановки – это использовать``tarantoolctl``:

$ tarantoolctl stop my_app

Последний шаг – удаление тестовых данных.

$ rm -R tarantool_test

Журналирование

Все важные события Tarantool записывает в файл журнала – например, в /var/log/tarantool/my_app.log. tarantoolctl строит путь до файла журнала следующим образом: «путь до директории с экземплярами» + «имя экземпляра» + «.lua».

Запишем что-нибудь в файл журнала:

$ tarantoolctl enter my_app
  /bin/tarantoolctl: connected to unix/:/var/run/tarantool/my_app.control
  unix/:/var/run/tarantool/my_app.control> require('log').info("Hello for the manual readers")
  ---
  ...

Затем проверим содержимое журнала:

$ tail /var/log/tarantool/my_app.log
  2017-04-04 15:54:04.977 [29255] main/101/tarantoolctl C> version 1.7.3-382-g68ef3f6a9
  2017-04-04 15:54:04.977 [29255] main/101/tarantoolctl C> log level 5
  2017-04-04 15:54:04.978 [29255] main/101/tarantoolctl I> mapping 134217728 bytes for tuple arena...
  2017-04-04 15:54:04.985 [29255] iproto/101/main I> binary: bound to [::1]:3301
  2017-04-04 15:54:04.986 [29255] main/101/tarantoolctl I> recovery start
  2017-04-04 15:54:04.986 [29255] main/101/tarantoolctl I> recovering from `/var/lib/tarantool/my_app/00000000000000000000.snap'
  2017-04-04 15:54:04.988 [29255] main/101/tarantoolctl I> ready to accept requests
  2017-04-04 15:54:04.988 [29255] main/101/tarantoolctl I> set 'checkpoint_interval' configuration option to 3600
  2017-04-04 15:54:04.988 [29255] main/101/my_app I> Run console at unix/:/var/run/tarantool/my_app.control
  2017-04-04 15:54:04.989 [29255] main/106/console/unix/:/var/ I> started
  2017-04-04 15:54:04.989 [29255] main C> entering the event loop
  2017-04-04 15:54:47.147 [29255] main/107/console/unix/: I> Hello for the manual readers

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

Файл /etc/logrotate.d/tarantool поставляется со стандартным дистрибутивом Tarantool. Его можно редактировать для изменения поведения по умолчанию. Содержимое файла обычно выглядит так:

/var/log/tarantool/*.log {
      daily
      size 512k
      missingok
      rotate 10
      compress
      delaycompress
      create 0640 tarantool adm
      postrotate
          /usr/bin/tarantoolctl logrotate `basename ${1%%.*}`
      endscript
  }

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

Tarantool может писать события в файл журнала, syslog или программу, указанную в конфигурационном файле (см. параметр log).

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

Безопасность

Tarantool разрешает два типа подключений:

  • Используя функцию console.listen() из модуля console, можно настроить порт для подключения к серверной административной консоли. Этот вариант для администраторов, которым необходимо подключиться к работающему экземпляру и послать некоторые запросы. tarantoolctl вызывает console.listen(), чтобы создать управляющий сокет для каждого запущенного экземпляра.
  • Используя параметр box.cfg{listen=…} из модуля box, можно настроить бинарный порт для соединений, которые читают и пишут в базу данных или вызывают хранимые процедуры.

Если вы подключены к административной консоли:

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

Поэтому порты для административной консоли следует настраивать очень осторожно. Если это TCP-порт, он должен быть открыть только для определенного IP-адреса. В идеале вместо TCP-порта лучше настроить доменный Unix-сокет, который требует наличие прав доступа к серверной машине. Тогда типичная настройка порта для административной консоли будет выглядеть следующим образом:

console.listen('/var/lib/tarantool/socket_name.sock')

а типичный URI для соединения будет таким:

/var/lib/tarantool/socket_name.sock

если у приемника событий есть права на запись в /var/lib/tarantool и у коннектора есть права на чтение из /var/lib/tarantool. Еще один способ подключиться к административной консоли экземпляра, запущенного с помощью tarantoolctl, – использовать tarantoolctl enter.

Выяснить, является ли некоторый TCP-порт портом для административной консоли, можно с помощью telnet. Например:

$ telnet 0 3303
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
Tarantool 1.10.0 (Lua console)
type 'help' for interactive help

В этом примере в ответе от сервера нет слова «binary» и есть слова «Lua console». Это значит, что мы успешно подключились к порту для административной консоли и можем вводить администраторские запросы на этом терминале.

Если вы подключены к бинарному порту:

  • Клиент-серверный протокол – бинарный.
  • Автоматически выбирается пользователь „guest“.
  • Для смены пользователя необходимо пройти аутентификацию.

Для удобства использования команда tarantoolctl connect автоматически определяет тип подключения при установке соединения и использует команду бинарного протокола EVAL для выполнения Lua-команд по бинарному подключению. Чтобы выполнить команду EVAL, аутентифицированный пользователь должен иметь глобальные «EXECUTE»-права.

Поэтому при невозможности подключиться к машине по ssh системный администратор может получить удаленный доступ к экземпляру, создав пользователя Tarantool с глобальными «EXECUTE»-правами и непустым паролем.

Просмотр состояния сервера

Использование Tarantool’а в качестве клиента

Tarantool входит в интерактивный режим, если:

Tarantool выводит приглашение командной строки (например, «tarantool>») – и вы можете посылать запросы. Если использовать Tarantool таким образом, он может выступать клиентом для удаленного сервера, см. простые примеры в Руководстве для начинающих.

Скрипт tarantoolctl использует интерактивный режим для реализации команд «enter» и «connect».

Выполнение кода на экземпляре Tarantool’а

Можно подключиться к административной консоли экземпляра и выполнить некий Lua-код с помощью утилиты tarantoolctl:

$ # для локальных экземпляров:
  $ tarantoolctl enter my_app
  /bin/tarantoolctl: Found my_app.lua in /etc/tarantool/instances.available
  /bin/tarantoolctl: Connecting to /var/run/tarantool/my_app.control
  /bin/tarantoolctl: connected to unix/:/var/run/tarantool/my_app.control
  unix/:/var/run/tarantool/my_app.control> 1 + 1
  ---
  - 2
  ...
  unix/:/var/run/tarantool/my_app.control>

  $ # для локальных и удаленных экземпляров:
  $ tarantoolctl connect username:password@127.0.0.1:3306

Можно также использовать tarantoolctl для выполнения Lua-кода на запущенном экземпляре Tarantool-сервера, не подключаясь к его административной консоли. Например:

$ # выполнение команд напрямую из командной строки
  $ <command> | tarantoolctl eval my_app
  <...>

  $ # - ИЛИ -

  $ # выполнение команд из скрипта
  $ tarantoolctl eval my_app script.lua
  <...>

Примечание

Еще можно использовать модули console и net.box из Tarantool-сервера. Также вы можете писать свои клиентские программы с использованием любого из доступных коннекторов. Однако большинство примеров в данном документе использует или tarantoolctl connect, или Tarantool-сервер как клиент.

Проверка состояния экземпляра

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

$ tarantoolctl status my_app
  my_app is running (pid: /var/run/tarantool/my_app.pid)

  $ # - ИЛИ -

  $ systemctl status tarantool@my_app
  tarantool@my_app.service - Tarantool Database Server
  Loaded: loaded (/etc/systemd/system/tarantool@.service; disabled; vendor preset: disabled)
  Active: active (running)
  Docs: man:tarantool(1)
  Process: 5346 ExecStart=/usr/bin/tarantoolctl start %I (code=exited, status=0/SUCCESS)
  Main PID: 5350 (tarantool)
  Tasks: 11 (limit: 512)
  CGroup: /system.slice/system-tarantool.slice/tarantool@my_app.service
  + 5350 tarantool my_app.lua <running>

Если вы используете систему, на которой доступна утилита systemd, выполните следующую команду для проверки содержимого журнала загрузки:

$ journalctl -u tarantool@my_app -n 5
  -- Logs begin at Fri 2016-01-08 12:21:53 MSK, end at Thu 2016-01-21 21:17:47 MSK. --
  Jan 21 21:17:47 localhost.localdomain systemd[1]: Stopped Tarantool Database Server.
  Jan 21 21:17:47 localhost.localdomain systemd[1]: Starting Tarantool Database Server...
  Jan 21 21:17:47 localhost.localdomain tarantoolctl[5969]: /usr/bin/tarantoolctl: Found my_app.lua in /etc/tarantool/instances.available
  Jan 21 21:17:47 localhost.localdomain tarantoolctl[5969]: /usr/bin/tarantoolctl: Starting instance...
  Jan 21 21:17:47 localhost.localdomain systemd[1]: Started Tarantool Database Server

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

  • box.cfg – проверка и указание всех конфигурационных параметров Tarantool-сервера,
  • box.slab – мониторинг использования и фрагментированности памяти, выделенной для хранения данных в Tarantool’е,
  • box.info – просмотр переменных Tarantool-сервера – в первую очередь тех, что относятся к репликации,
  • box.stat – просмотр статистики Tarantool’а по запросам и использованию сети,

Можно также попробовать воспользоваться Lua-модулем tarantool/prometheus, который облегчает сбор метрик (например, использование памяти или количество запросов) с Tarantool-приложений и баз данных и их публикацию через протокол Prometheus.

Пример

Очень часто администраторам приходится вызывать функцию box.slab.info(), которая показывает подробную статистику по использованию памяти для конкретного экземпляра Tarantool’а.

tarantool> box.slab.info()
  ---
  - items_size: 228128
    items_used_ratio: 1.8%
    quota_size: 1073741824
    quota_used_ratio: 0.8%
    arena_used_ratio: 43.2%
    items_used: 4208
    quota_used: 8388608
    arena_size: 2325176
    arena_used: 1003632
  ...

Tarantool занимает память операционной системы, например, когда пользователь вставляет много данных. Можно проверить, сколько памяти занято, выполнив команду (в Linux):

ps -eo args,%mem | grep "tarantool"

Tarantool almost never releases this memory, even if the user deletes everything that was inserted, or reduces fragmentation by calling the Lua garbage collector via the collectgarbage function.

Как правило, это не влияет на производительность. Однако, чтобы заставить Tarantool высвободить память, можно вызвать box.snapshot, остановить экземпляр и перезапустить его.

Профилирование производительности

Иногда Tarantool может работать медленнее, чем обычно. Причин такого поведения может быть несколько: проблемы с диском, Lua-скрипты, активно использующие процессор, или неправильная настройка. В таких случаях в журнале Tarantool’а могут отсутствовать необходимые подробности, поэтому единственным признаком неправильного поведения является наличие в журнале записей вида W> too long DELETE: 8.546 sec. Ниже приведены инструменты и приемы, которые облегчают снятие профиля производительности Tarantool’а. Эта процедура может помочь при решении проблем с замедлением.

Примечание

Большинство инструментов, за исключением fiber.info(), предназначено для дистрибутивов GNU/Linux, но не для FreeBSD или Mac OS.

fiber.info()

Самый простой способ профилирования – это использование встроенных функций Tarantool’а. fiber.info() возвращает информацию обо всех работающих файберах с соответствующей трассировкой стека для языка C. Эти данные показывают, сколько файберов запущенно на данный момент и какие функции, написанные на C, вызываются чаще остальных.

Сначала войдите в интерактивную административную консоль вашего экземпляра Tarantool’а:

$ tarantoolctl enter NAME

После этого загрузите модуль fiber:

tarantool> fiber = require('fiber')

Теперь можно получить необходимую информацию с помощью fiber.info().

На этом шаге в вашей консоли должно выводиться следующее:

tarantool> fiber = require('fiber')
  ---
  ...
  tarantool> fiber.info()
  ---
  - 360:
      csw: 2098165
      backtrace:
      - '#0 0x4d1b77 in wal_write(journal*, journal_entry*)+487'
      - '#1 0x4bbf68 in txn_commit(txn*)+152'
      - '#2 0x4bd5d8 in process_rw(request*, space*, tuple**)+136'
      - '#3 0x4bed48 in box_process1+104'
      - '#4 0x4d72f8 in lbox_replace+120'
      - '#5 0x50f317 in lj_BC_FUNCC+52'
      fid: 360
      memory:
        total: 61744
        used: 480
      name: main
    129:
      csw: 113
      backtrace: []
      fid: 129
      memory:
        total: 57648
        used: 0
      name: 'console/unix/:'
  ...

Мы рекомендуем присваивать создаваемым файберам понятные имена, чтобы их можно было легко найти в списке, выводимом fiber.info(). В примере ниже создается файбер с именем myworker:

tarantool> fiber = require('fiber')
  ---
  ...
  tarantool> f = fiber.create(function() while true do fiber.sleep(0.5) end end)
  ---
  ...
  tarantool> f:name('myworker') <!-- присваивание имени файберу
  ---
  ...
  tarantool> fiber.info()
  ---
  - 102:
      csw: 14
      backtrace:
      - '#0 0x501a1a in fiber_yield_timeout+90'
      - '#1 0x4f2008 in lbox_fiber_sleep+72'
      - '#2 0x5112a7 in lj_BC_FUNCC+52'
      fid: 102
      memory:
        total: 57656
        used: 0
      name: myworker <!-- новый созданный фоновый файбер
    101:
      csw: 284
      backtrace: []
      fid: 101
      memory:
        total: 57656
        used: 0
      name: interactive
  ...

Для принудительного завершения файбера используется команда fiber.kill(fid):

tarantool> fiber.kill(102)
  ---
  ...
  tarantool> fiber.info()
  ---
  - 101:
      csw: 324
      backtrace: []
      fid: 101
      memory:
        total: 57656
        used: 0
      name: interactive
  ...

Если вам необходимо динамически получать информацию с помощью fiber.info(), вам может пригодиться приведенный ниже скрипт. Он каждые полсекунды подключается к экземпляру Tarantool’а, указанному в переменной NAME, выполняет команду fiber.info() и записывает ее выход в файл fiber-info.txt:

$ rm -f fiber.info.txt
  $ watch -n 0.5 "echo 'require(\"fiber\").info()' | tarantoolctl enter NAME | tee -a fiber-info.txt"

Если вы не можете самостоятельно разобраться, какой именно файбер вызывает проблемы с производительностью, запустите данный скрипт на 10-15 секунд и пришлите получившийся файл команде Tarantool’а на адрес support@tarantool.org.

Простейшие профилировщики

pstack <pid>

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

После установки воспользуйтесь следующей командой:

$ pstack $(pidof tarantool INSTANCENAME.lua)

Затем выполните:

$ echo $(pidof tarantool INSTANCENAME.lua)

чтобы вывести на экран PID экземпляра Tarantool’а, использующего файл INSTANCENAME.lua.

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

Thread 19 (Thread 0x7f09d1bff700 (LWP 24173)):
  #0 0x00007f0a1a5423f2 in ?? () from /lib64/libgomp.so.1
  #1 0x00007f0a1a53fdc0 in ?? () from /lib64/libgomp.so.1
  #2 0x00007f0a1ad5adc5 in start_thread () from /lib64/libpthread.so.0
  #3 0x00007f0a1a050ced in clone () from /lib64/libc.so.6
  Thread 18 (Thread 0x7f09d13fe700 (LWP 24174)):
  #0 0x00007f0a1a5423f2 in ?? () from /lib64/libgomp.so.1
  #1 0x00007f0a1a53fdc0 in ?? () from /lib64/libgomp.so.1
  #2 0x00007f0a1ad5adc5 in start_thread () from /lib64/libpthread.so.0
  #3 0x00007f0a1a050ced in clone () from /lib64/libc.so.6
  <...>
  Thread 2 (Thread 0x7f09c8bfe700 (LWP 24191)):
  #0 0x00007f0a1ad5e6d5 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  #1 0x000000000045d901 in wal_writer_pop(wal_writer*) ()
  #2 0x000000000045db01 in wal_writer_f(__va_list_tag*) ()
  #3 0x0000000000429abc in fiber_cxx_invoke(int (*)(__va_list_tag*), __va_list_tag*) ()
  #4 0x00000000004b52a0 in fiber_loop ()
  #5 0x00000000006099cf in coro_init ()
  Thread 1 (Thread 0x7f0a1c47fd80 (LWP 24172)):
  #0 0x00007f0a1a0512c3 in epoll_wait () from /lib64/libc.so.6
  #1 0x00000000006051c8 in epoll_poll ()
  #2 0x0000000000607533 in ev_run ()
  #3 0x0000000000428e13 in main ()

gdb -ex «bt» -p <pid>

Как и в случае с pstack, перед использованием GNU-отладчик (также известный как gdb) необходимо сначала установить через пакетный менеджер, встроенный в ваш дистрибутив Linux.

После установки воспользуйтесь следующей командой:

$ gdb -ex "set pagination 0" -ex "thread apply all bt" --batch -p $(pidof tarantool INSTANCENAME.lua)

Затем выполните:

$ echo $(pidof tarantool INSTANCENAME.lua)

чтобы вывести на экран PID экземпляра Tarantool’а, использующего файл INSTANCENAME.lua.

После использования отладчика в консоль должна выводиться следующая информация:

[Thread debugging using libthread_db enabled]
  Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

  [CUT]

  Thread 1 (Thread 0x7f72289ba940 (LWP 20535)):
  #0 _int_malloc (av=av@entry=0x7f7226e0eb20 <main_arena>, bytes=bytes@entry=504) at malloc.c:3697
  #1 0x00007f7226acf21a in __libc_calloc (n=<optimized out>, elem_size=<optimized out>) at malloc.c:3234
  #2 0x00000000004631f8 in vy_merge_iterator_reserve (capacity=3, itr=0x7f72264af9e0) at /usr/src/tarantool/src/box/vinyl.c:7629
  #3 vy_merge_iterator_add (itr=itr@entry=0x7f72264af9e0, is_mutable=is_mutable@entry=true, belong_range=belong_range@entry=false) at /usr/src/tarantool/src/box/vinyl.c:7660
  #4 0x00000000004703df in vy_read_iterator_add_mem (itr=0x7f72264af990) at /usr/src/tarantool/src/box/vinyl.c:8387
  #5 vy_read_iterator_use_range (itr=0x7f72264af990) at /usr/src/tarantool/src/box/vinyl.c:8453
  #6 0x000000000047657d in vy_read_iterator_start (itr=<optimized out>) at /usr/src/tarantool/src/box/vinyl.c:8501
  #7 0x00000000004766b5 in vy_read_iterator_next (itr=itr@entry=0x7f72264af990, result=result@entry=0x7f72264afad8) at /usr/src/tarantool/src/box/vinyl.c:8592
  #8 0x000000000047689d in vy_index_get (tx=tx@entry=0x7f7226468158, index=index@entry=0x2563860, key=<optimized out>, part_count=<optimized out>, result=result@entry=0x7f72264afad8) at /usr/src/tarantool/src/box/vinyl.c:5705
  #9 0x0000000000477601 in vy_replace_impl (request=<optimized out>, request=<optimized out>, stmt=0x7f72265a7150, space=0x2567ea0, tx=0x7f7226468158) at /usr/src/tarantool/src/box/vinyl.c:5920
  #10 vy_replace (tx=0x7f7226468158, stmt=stmt@entry=0x7f72265a7150, space=0x2567ea0, request=<optimized out>) at /usr/src/tarantool/src/box/vinyl.c:6608
  #11 0x00000000004615a9 in VinylSpace::executeReplace (this=<optimized out>, txn=<optimized out>, space=<optimized out>, request=<optimized out>) at /usr/src/tarantool/src/box/vinyl_space.cc:108
  #12 0x00000000004bd723 in process_rw (request=request@entry=0x7f72265a70f8, space=space@entry=0x2567ea0, result=result@entry=0x7f72264afbc8) at /usr/src/tarantool/src/box/box.cc:182
  #13 0x00000000004bed48 in box_process1 (request=0x7f72265a70f8, result=result@entry=0x7f72264afbc8) at /usr/src/tarantool/src/box/box.cc:700
  #14 0x00000000004bf389 in box_replace (space_id=space_id@entry=513, tuple=<optimized out>, tuple_end=<optimized out>, result=result@entry=0x7f72264afbc8) at /usr/src/tarantool/src/box/box.cc:754
  #15 0x00000000004d72f8 in lbox_replace (L=0x413c5780) at /usr/src/tarantool/src/box/lua/index.c:72
  #16 0x000000000050f317 in lj_BC_FUNCC ()
  #17 0x00000000004d37c7 in execute_lua_call (L=0x413c5780) at /usr/src/tarantool/src/box/lua/call.c:282
  #18 0x000000000050f317 in lj_BC_FUNCC ()
  #19 0x0000000000529c7b in lua_cpcall ()
  #20 0x00000000004f6aa3 in luaT_cpcall (L=L@entry=0x413c5780, func=func@entry=0x4d36d0 <execute_lua_call>, ud=ud@entry=0x7f72264afde0) at /usr/src/tarantool/src/lua/utils.c:962
  #21 0x00000000004d3fe7 in box_process_lua (handler=0x4d36d0 <execute_lua_call>, out=out@entry=0x7f7213020600, request=request@entry=0x413c5780) at /usr/src/tarantool/src/box/lua/call.c:382
  #22 box_lua_call (request=request@entry=0x7f72130401d8, out=out@entry=0x7f7213020600) at /usr/src/tarantool/src/box/lua/call.c:405
  #23 0x00000000004c0f27 in box_process_call (request=request@entry=0x7f72130401d8, out=out@entry=0x7f7213020600) at /usr/src/tarantool/src/box/box.cc:1074
  #24 0x000000000041326c in tx_process_misc (m=0x7f7213040170) at /usr/src/tarantool/src/box/iproto.cc:942
  #25 0x0000000000504554 in cmsg_deliver (msg=0x7f7213040170) at /usr/src/tarantool/src/cbus.c:302
  #26 0x0000000000504c2e in fiber_pool_f (ap=<error reading variable: value has been optimized out>) at /usr/src/tarantool/src/fiber_pool.c:64
  #27 0x000000000041122c in fiber_cxx_invoke(fiber_func, typedef __va_list_tag __va_list_tag *) (f=<optimized out>, ap=<optimized out>) at /usr/src/tarantool/src/fiber.h:645
  #28 0x00000000005011a0 in fiber_loop (data=<optimized out>) at /usr/src/tarantool/src/fiber.c:641
  #29 0x0000000000688fbf in coro_init () at /usr/src/tarantool/third_party/coro/coro.c:110

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

$ rm -f stack-trace.txt
  $ watch -n 0.5 "gdb -ex 'set pagination 0' -ex 'thread apply all bt' --batch -p $(pidof tarantool INSTANCENAME.lua) | tee -a stack-trace.txt"

С точки зрения структуры и функциональности, этот скрипт идентичен тому, что используется выше с fiber.info().

Если вам не удается отыскать причину пониженной производительности, запустите данный скрипт на 10-15 секунд и пришлите получившийся файл stack-trace.txt команде Tarantool’а на адрес support@tarantool.org.

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

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

gperftools

Чтобы использовать профилировщик процессора из набора Google Performance Tools с Tarantool’ом, необходимо сначала установить зависимости:

  • Если вы используете Debian/Ubuntu, запустите эту команду:
$ apt-get install libgoogle-perftools4
  • Если вы используете RHEL/CentOS/Fedora, запустите эту команду:
$ yum install gperftools-libs

После этого установите привязки для Lua:

$ tarantoolctl rocks install gperftools

После окончания установки войдите в интерактивную административную консоль вашего экземпляра Tarantool’а:

$ tarantoolctl enter NAME

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

tarantool> cpuprof = require('gperftools.cpu')
  tarantool> cpuprof.start('/home/<имя_пользователя>/tarantool-on-production.prof')

На сбор метрик производительности у профилировщика уходит по крайней мере пара минут. По истечении этого времени можно сохранять информацию на диск (неограниченное количество раз):

tarantool> cpuprof.flush()

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

tarantool> cpuprof.stop()

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

$ pprof --text /usr/bin/tarantool /home/<имя_пользователя>/tarantool-on-production.prof

Примечание

В дистрибутивах Debian/Ubuntu утилита pprof называется google-pprof.

В консоль должно выводиться приблизительно следующее:

Total: 598 samples
        83 13.9% 13.9% 83 13.9% epoll_wait
        54 9.0% 22.9% 102 17.1%
  vy_mem_tree_insert.constprop.35
        32 5.4% 28.3% 34 5.7% __write_nocancel
        28 4.7% 32.9% 42 7.0% vy_mem_iterator_start_from
        26 4.3% 37.3% 26 4.3% _IO_str_seekoff
        21 3.5% 40.8% 21 3.5% tuple_compare_field
        19 3.2% 44.0% 19 3.2%
  ::TupleCompareWithKey::compare
        19 3.2% 47.2% 38 6.4% tuple_compare_slowpath
        12 2.0% 49.2% 23 3.8% __libc_calloc
         9 1.5% 50.7% 9 1.5%
  ::TupleCompare::compare@42efc0
         9 1.5% 52.2% 9 1.5% vy_cache_on_write
         9 1.5% 53.7% 57 9.5% vy_merge_iterator_next_key
         8 1.3% 55.0% 8 1.3% __nss_passwd_lookup
         6 1.0% 56.0% 25 4.2% gc_onestep
         6 1.0% 57.0% 6 1.0% lj_tab_next
         5 0.8% 57.9% 5 0.8% lj_alloc_malloc
         5 0.8% 58.7% 131 21.9% vy_prepare
perf

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

Примечание

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

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

$ perf record -g -p $(pidof tarantool INSTANCENAME.lua)

Эта команда сохраняет собранные данные в файл perf.data, который находится в текущей рабочей папке. Для остановки процесса (обычно через 10-15 секунд) нажмите ctrl+C. В консоли должно появиться следующее:

^C[ perf record: Woken up 1 times to write data ]
  [ perf record: Captured and wrote 0.225 MB perf.data (1573 samples) ]

Затем выполните эту команду:

$ perf report -n -g --stdio | tee perf-report.txt

Она превращает содержащиеся в perf.data статистические данные в отчет о производительности, который сохраняется в файл perf-report.txt.

Получившийся отчет выглядит следующим образом:

# Samples: 14K of event 'cycles'
  # Event count (approx.): 9927346847
  #
  # Children Self Samples Command Shared Object Symbol
  # ........ ........ ............ ......... .................. .......................................
  #
      35.50% 0.55% 79 tarantool tarantool [.] lj_gc_step
              |
               --34.95%--lj_gc_step
                         |
                         |--29.26%--gc_onestep
                         | |
                         | |--13.85%--gc_sweep
                         | | |
                         | | |--5.59%--lj_alloc_free
                         | | |
                         | | |--1.33%--lj_tab_free
                         | | | |
                         | | | --1.01%--lj_alloc_free
                         | | |
                         | | --1.17%--lj_cdata_free
                         | |
                         | |--5.41%--gc_finalize
                         | | |
                         | | |--1.06%--lj_obj_equal
                         | | |
                         | | --0.95%--lj_tab_set
                         | |
                         | |--4.97%--rehashtab
                         | | |
                         | | --3.65%--lj_tab_resize
                         | | |
                         | | |--0.74%--lj_tab_set
                         | | |
                         | | --0.72%--lj_tab_newkey
                         | |
                         | |--0.91%--propagatemark
                         | |
                         | --0.67%--lj_cdata_free
                         |
                          --5.43%--propagatemark
                                    |
                                     --0.73%--gc_mark

Инструменты gperftools и perf отличаются от pstack и gdb низкой затратой ресурсов (пренебрежимо малой по сравнению с pstack и gdb): они подключаются к работающим процессам без больших задержек, а потому могут использоваться без серьезных последствий.

Контроль за фоновыми программами

Сигналы от сервера

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

Сигнал Эффект
SIGHUP Может привести к ротации журналов, см. пример в справочнике по параметрам журналирования Tarantool’а.
SIGUSR1 Может привести к созданию снимка состояния базы данных, см. описание функции box.snapshot.
SIGTERM Может привести к корректному завершению работы (с предварительным сохранением всех данных).
SIGINT (или «прерывание от клавиатуры») Может привести к корректному завершению работы.
SIGKILL Приводит к аварийному завершению работы.

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

Автоматическая перезагрузка экземпляра

На платформах, где доступна утилита systemd, systemd автоматически перезагружает все экземпляры Tarantool’а при сбое. Чтобы продемонстрировать это, отключим один из экземпляров:

$ systemctl status tarantool@my_app|grep PID
  Main PID: 5885 (tarantool)
  $ tarantoolctl enter my_app
  /bin/tarantoolctl: Found my_app.lua in /etc/tarantool/instances.available
  /bin/tarantoolctl: Connecting to /var/run/tarantool/my_app.control
  /bin/tarantoolctl: connected to unix/:/var/run/tarantool/my_app.control
  unix/:/var/run/tarantool/my_app.control> os.exit(-1)
  /bin/tarantoolctl: unix/:/var/run/tarantool/my_app.control: Remote host closed connection

А теперь убедимся, что systemd перезапустила его:

$ systemctl status tarantool@my_app|grep PID
  Main PID: 5914 (tarantool)

И под конец проверим содержимое журнала загрузки:

$ journalctl -u tarantool@my_app -n 8
  -- Записи начинаются в пятницу 08.01.2016 12:21:53 MSK, заканчиваются в четверг 21.01.2016 2016-01-21 21:09:45 MSK. --
  Jan 21 21:09:45 localhost.localdomain systemd[1]: tarantool@my_app.service: Unit entered failed state.
  Jan 21 21:09:45 localhost.localdomain systemd[1]: tarantool@my_app.service: Failed with result 'exit-code'.
  Jan 21 21:09:45 localhost.localdomain systemd[1]: tarantool@my_app.service: Service hold-off time over, scheduling restart.
  Jan 21 21:09:45 localhost.localdomain systemd[1]: Stopped Tarantool Database Server.
  Jan 21 21:09:45 localhost.localdomain systemd[1]: Starting Tarantool Database Server...
  Jan 21 21:09:45 localhost.localdomain tarantoolctl[5910]: /usr/bin/tarantoolctl: Found my_app.lua in /etc/tarantool/instances.available
  Jan 21 21:09:45 localhost.localdomain tarantoolctl[5910]: /usr/bin/tarantoolctl: Starting instance...
  Jan 21 21:09:45 localhost.localdomain systemd[1]: Started Tarantool Database Server.

Создание дампов памяти

Tarantool создает дамп памяти при получении одного из следующих сигналов: SIGSEGV, SIGFPE, SIGABRT или SIGQUIT. При сбое Tarantool’а дамп создается автоматически.

На платформах, где доступна утилита systemd, coredumpctl автоматически сохраняет дампы памяти и трассировку стека при аварийном завершении Tarantool-сервера. Вот как включить создание дампов памяти в Unix-системе:

  1. Убедитесь, что лимиты для сессии установлены таким образом, чтобы можно было создавать дампы памяти, – выполните команду ulimit -c unlimited. Также проверьте «man 5 core» на другие причины, по которым дамп памяти может не создаваться.
  2. Создайте директорию для записи дампов памяти и убедитесь, что в эту директорию действительно можно производить запись. На Linux путь до директории задается в параметре ядра, который настраивается через /proc/sys/kernel/core_pattern.
  3. Убедитесь, что дампы памяти включают трассировку стека. При использовании бинарного дистрибутива Tarantool’а эта информация включается автоматически. При сборке Tarantool’а из исходников, если передать CMake флаг -DCMAKE_BUILD_TYPE=Release, вы не получите подробной информации.

Для симуляции сбоя можно попытаться выполнить нелегальную команду на работающем экземпляре Tarantool’а:

$ # !!! пожалуйста, никогда не делайте этого на боевом сервере !!!
  $ tarantoolctl enter my_app
  unix/:/var/run/tarantool/my_app.control> require('ffi').cast('char *', 0)[0] = 48
  /bin/tarantoolctl: unix/:/var/run/tarantool/my_app.control: Remote host closed connection

Есть другой способ: если вы знаете PID экземпляра ($PID в нашем примере), можно остановить этот экземпляр, запустив отладчик gdb:

$ gdb -batch -ex "generate-core-file" -p $PID

или послав вручную сигнал SIGABRT:

$ kill -SIGABRT $PID

Примечание

Чтобы узнать PID экземпляра, можно:

  • посмотреть его с помощью box.info.pid,
  • использовать команду ps -A | grep tarantool, или
  • выполнить systemctl status tarantool@my_app|grep PID.

Чтобы посмотреть на последние сбои Tarantool-демона на платформах, где доступна утилита systemd, выполните команду:

$ coredumpctl list /usr/bin/tarantool
  MTIME                            PID   UID   GID SIG PRESENT EXE
  Sat 2016-01-23 15:21:24 MSK   20681  1000  1000   6   /usr/bin/tarantool
  Sat 2016-01-23 15:51:56 MSK   21035   995   992   6   /usr/bin/tarantool

Чтобы сохранить дамп памяти в файл, выполните команду:

$ coredumpctl -o filename.core info <pid>

Трассировка стека

Так как Tarantool хранит кортежи в памяти, файлы с дампами памяти могут быть довольно большими. Чтобы найти проблему, обычно целый файл не нужен – достаточно только «трассировки стека» или «обратной трассировки».

Чтобы сохранить трассировку стека в файл, выполните команду:

$ gdb -se "tarantool" -ex "bt full" -ex "thread apply all bt" --batch -c core> /tmp/tarantool_trace.txt

где:

  • «tarantool» – это путь до исполняемого файла Tarantool’а,
  • «core» – это путь до файла с дампом памяти, и
  • «/tmp/tarantool_trace.txt» – это пример пути до файла, в который сохраняется трассировка стека.

Примечание

Иногда может оказаться, что файл с трассировкой стека не содержит отладочных символов – в таких строках вместо имени будет стоять ”??”. Если это произошло, ознакомьтесь с инструкциями на этих двух wiki-страницах Tarantool’а: How to debug core dump of stripped tarantool и How to debug core from different OS.

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

$ coredumpctl info 21035
            PID: 21035 (tarantool)
            UID: 995 (tarantool)
            GID: 992 (tarantool)
         Signal: 6 (ABRT)
      Timestamp: Sat 2016-01-23 15:51:42 MSK (4h 36min ago)
   Command Line: tarantool my_app.lua <running>
     Executable: /usr/bin/tarantool
  Control Group: /system.slice/system-tarantool.slice/tarantool@my_app.service
           Unit: tarantool@my_app.service
          Slice: system-tarantool.slice
        Boot ID: 7c686e2ef4dc4e3ea59122757e3067e2
     Machine ID: a4a878729c654c7093dc6693f6a8e5ee
       Hostname: localhost.localdomain
        Message: Process 21035 (tarantool) of user 995 dumped core.

                 Stack trace of thread 21035:
                 #0  0x00007f84993aa618 raise (libc.so.6)
                 #1  0x00007f84993ac21a abort (libc.so.6)
                 #2  0x0000560d0a9e9233 _ZL12sig_fatal_cbi (tarantool)
                 #3  0x00007f849a211220 __restore_rt (libpthread.so.0)
                 #4  0x0000560d0aaa5d9d lj_cconv_ct_ct (tarantool)
                 #5  0x0000560d0aaa687f lj_cconv_ct_tv (tarantool)
                 #6  0x0000560d0aaabe33 lj_cf_ffi_meta___newindex (tarantool)
                 #7  0x0000560d0aaae2f7 lj_BC_FUNCC (tarantool)
                 #8  0x0000560d0aa9aabd lua_pcall (tarantool)
                 #9  0x0000560d0aa71400 lbox_call (tarantool)
                 #10 0x0000560d0aa6ce36 lua_fiber_run_f (tarantool)
                 #11 0x0000560d0a9e8d0c _ZL16fiber_cxx_invokePFiP13__va_list_tagES0_ (tarantool)
                 #12 0x0000560d0aa7b255 fiber_loop (tarantool)
                 #13 0x0000560d0ab38ed1 coro_init (tarantool)
                 ...

Отладчик

Для запуска отладчика gdb, выполните команду:

$ coredumpctl gdb <pid>

Мы очень рекомендуем установить пакет tarantool-debuginfo, чтобы сделать отладку средствами gdb более эффективной. Например:

$ dnf debuginfo-install tarantool

С помощью gdb можно узнать, какие еще debuginfo-пакеты нужно установить:

$ gdb -p <pid>
  ...
  Missing separate debuginfos, use: dnf debuginfo-install
  glibc-2.22.90-26.fc24.x86_64 krb5-libs-1.14-12.fc24.x86_64
  libgcc-5.3.1-3.fc24.x86_64 libgomp-5.3.1-3.fc24.x86_64
  libselinux-2.4-6.fc24.x86_64 libstdc++-5.3.1-3.fc24.x86_64
  libyaml-0.1.6-7.fc23.x86_64 ncurses-libs-6.0-1.20150810.fc24.x86_64
  openssl-libs-1.0.2e-3.fc24.x86_64

В трассировке стека присутствуют символические имена, даже если у вас не установлен пакет tarantool-debuginfo.

Аварийное восстановление

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

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

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

Мастер-реплика

Конфигурация: один мастер и одна реплика.

Проблема: мастер вышел из строя.

План действий:

  1. Убедитесь, что мастер полностью остановлен. Например, подключитесь к мастеру и используйте команду systemctl stop tarantool@<имя_экземпляра>.
  2. Переключите реплику в режим мастера, установив параметру box.cfg.read_only значение false. Теперь вся нагрузка пойдет только на реплику (по сути ставшую мастером).
  3. Настройте на свободной машине замену вышедшему из строя мастеру, установив параметру replication в качестве значения URI реплики (которая в данный момент выполняет роль мастера), чтобы новая реплика начала синхронизироваться с текущим мастером. Значение параметра box.cfg.read_only в новом экземпляре должно быть установлено на true.

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

  1. Узнайте позицию вышедшего из строя мастера – эта информация доступна из нового мастера.

    1. Посмотрите UUID экземпляра в xlog-файле вышедшего из строя мастера:

      $ head -5 *.xlog | grep Instance
        Instance: ed607cad-8b6d-48d8-ba0b-dae371b79155
      
    2. Используйте этот UUID на новом мастере для поиска позиции:

      tarantool> box.info.vclock[box.space._cluster.index.uuid:select{'ed607cad-8b6d-48d8-ba0b-dae371b79155'}[1][1]]
        ---
        - 23425
        <...>
      
  2. Запишите транзакции из .xlog-файла вышедшего из строя мастера в новый мастер, начиная с позиции нового мастера:

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

      tarantool> box.space._cluster:select{}
        ---
        - - [1, '88580b5c-4474-43ab-bd2b-2409a9af80d2']
        ...
      
    2. Запишите транзакции в новый мастер:

      $ tarantoolctl <uri_нового_мастера> <xlog_файл> play --from-lsn 23425 --replica 1
      

Мастер-мастер

Конфигурация: два мастера.

Проблема: мастер #1 вышел из строя.

План действий:

  1. Пусть вся нагрузка идет только на мастер #2 (действующий мастер).

2. Follow the same steps as in the master-replica recovery scenario to create a new master and salvage lost data.

Потеря данных

Конфигурация: мастер-мастер или мастер-реплика.

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

Эта инструкция применима только для данных, хранящихся на движке memtx. План действий:

  1. Put all nodes in read-only mode and disable checkpointing with box.backup.start(). Disabling the checkpointing is necessary to prevent the Tarantool garbage collector from removing files made with older checkpoints.
  2. Возьмите последний корректный .snap-файл и, используя команду tarantoolctl cat, выясните, на каком именно lsn произошла потеря данных.
  3. Запустите новый экземпляр (экземпляр #1) и с помощью команды tarantoolctl play скопируйте в него содержимое .snap/.xlog-файлов вплоть до вычисленного lsn.
  4. Настройте новую реплику с помощью восстановленного мастера (экземпляра #1).

Резервное копирование

Tarantool has an append-only storage architecture: it appends data to files but it never overwrites earlier data. The Tarantool garbage collector removes old files after a checkpoint. You can prevent or delay the garbage collector’s action by configuring the checkpoint daemon. Backups can be taken at any time, with minimal overhead on database performance.

backup.start() and backup.stop()

Two functions are helpful for backups in certain situations.

box.backup.start() informs the server that some activities that might interfer with backup should be suspended – suspend checkpointing, suspend Tarantool garbage collection, and effectively enter read-only mode.

Later box.backup.stop() informs the server that normal operations may resume. Starting with Tarantool 1.10.1 there is a new optional argument, box.backup.start(n), where n indicates the checkpoint to use relative to the latest checkpoint – for example n = 0 means «backup will be based on the latest checkpoint», n = 1 means «backup will be based on the first checkpoint before the latest checkpoint (counting backwards)», and so on, and the default value for n is zero.

box.backup.start() returns a table with the names of snapshot and vinyl files that should be copied. Example:

tarantool> box.backup.start()
---
- - ./00000000000000000015.snap
  - ./00000000000000000000.vylog
  - ./513/0/00000000000000000002.index
  - ./513/0/00000000000000000002.run
...

Горячее резервирование (memtx)

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

Последний созданный Tarantool’ом файл-снимок является резервной копией всей базы данных; а WAL-файлы, созданные следом за последним файлом-снимком, являются инкрементными копиями. Поэтому процедура резервирования сводится к копированию последнего файла-снимка и следующих за ним WAL-файлов.

  1. С помощью tar создайте (зачастую сжатую) копию последнего .snap-файла и следующих за ним .xlog-файлов из директорий memtx_dir и wal_dir.
  2. Если того требуют правила безопасности, зашифруйте получившийся .tar-файл.
  3. Скопируйте .tar-файл в надежное место.

В дальнейшем базу данных можно восстановить, разархивировав содержимое .tar-файла в директории memtx_dir и wal_dir.

Горячее резервирование (vinyl/memtx)

Vinyl stores its files in vinyl_dir, and creates a folder for each database space. Dump and compaction processes are append-only and create new files. The Tarantool garbage collector may remove old files after each checkpoint.

Для создания смешанной резервной копии:

  1. Issue box.backup.start() on the administrative console. This will suspend garbage collection till the next box.backup.stop() and will return a list of files to back up.
  2. Скопируйте файлы из списка в надежное место. Это касается файлов-снимков memtx, выполняемых vinyl-файлов и индексных файлов, соответствующих последней контрольной точке.
  3. Issue box.backup.stop() so the garbage collector can continue.

Непрерывное удаленное резервирование

Репликация используется не только для резервирования, но и для выравнивания нагрузки.

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

Непрерывное резервирование

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

Для этого нужна специальная утилита для копирования файлов (например, rsync), которая позволит удаленно и на постоянной основе копировать только изменившиеся части WAL-файла, а не весь файл целиком.

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

Обновление

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

Если вы создали базу данных в старой версии Tarantool’а, а потом обновили Tarantool до более свежей версии, вызовите команду box.schema.upgrade(). Она обновляет системные спейсы Tarantool’а так, чтобы они совпадали с текущей установленной версией Tarantool’а.

Например, вот что происходит, если выполнить команду box.schema.upgrade() для базы данных, созданной в Tarantool версии 1.6.4 (показана лишь малая часть выводимых сообщений):

tarantool> box.schema.upgrade()
 alter index primary on _space set options to {"unique":true}, parts to [[0,"unsigned"]]
 alter space _schema set options to {}
 create view _vindex...
 grant read access to 'public' role for _vindex view
 set schema version to 1.7.0
 ---
 ...

Обновление экземпляра Tarantool’а

Tarantool поддерживает обратную совместимость между двумя последовательными версиями. Например, обновление Tarantool 1.6 до 1.7 или Tarantool 1.7 до 1.8 не должно вызвать затруднений, тогда как миграции с Tarantool 1.6 прямиком на 1.8 могут препятствовать несовместимые изменения.

How to upgrade from Tarantool 1.6 to 1.7 / 1.10

This procedure is for upgrading a standalone Tarantool instance in production from 1.6.x to 1.7.x (or to 1.10.x). Notice that this will always imply a downtime. To upgrade without downtime, you need several Tarantool servers running in a replication cluster (see below).

Tarantool 1.7 has an incompatible .snap and .xlog file format: 1.6 files are supported during upgrade, but you won’t be able to return to 1.6 after running under 1.7 for a while. It also renames a few configuration parameters, but old parameters are supported. The full list of breaking changes is available in release notes for Tarantool 1.7 / 1.9 / 1.10.

To upgrade from Tarantool 1.6 to 1.7 (or to 1.10.x):

  1. Check with application developers whether application files need to be updated due to incompatible changes (see 1.7 / 1.9 / 1.10 release notes). If yes, back up the old application files.
  2. Остановите Tarantool-сервер.
  3. Создайте копию всех данных (см. подразделы про горячее резервное копирование в разделе Резервное копирование) и пакета, из которого была установлена текущая (старая) версия (на случай отката).
  4. Обновите Tarantool-сервер. Инструкции по установке доступны на странице загрузок Tarantool’а.
  5. Обновите базу данных Tarantool. Выполните команду box.schema.upgrade(), поместив ее внутрь функции box.once() в файле инициализации Tarantool’а. В результате на этапе запуска Tarantool создаст новые системные спейсы, обновит названия типов данных (например, num -> unsigned, str -> string) и список доступных типов данных в системных спейсах.
  6. При необходимости обновите файлы приложения.
  7. Запустите обновленный Tarantool-сервер с помощью tarantoolctl или systemctl.

Обновление Tarantool’а в репликационном кластере

Tarantool 1.7 (as well as Tarantool 1.9 and 1.10) can work as a replica for Tarantool 1.6 and vice versa. Replicas perform capability negotiation on handshake, and new 1.7 replication features are not used with 1.6 replicas. This allows upgrading clustered configurations.

Этот процесс позволяет осуществить последовательное обновление без простоев и подходит для любой конфигурации кластера: master-master или мастер-реплика.

  1. Обновите Tarantool на всех репликах (или на любом мастере в кластере мастер-мастер). Подробные инструкции доступны в подразделе Обновление экземпляра Tarantool’а.

  2. Проверьте работу реплик:

    1. Запустите Tarantool.
    2. Присоединитесь к мастеру и начните работать, как раньше.

    На мастере установлена старая версия Tarantool’а, которая всегда совместима со следующей мажорной версией.

  3. Обновите мастер. Процесс такой же, как и при обновлении реплики.

  4. Проверьте работу мастера:

    1. Запустите Tarantool в режиме реплики для получения последней версии данных.
    2. Переключитесь в режим мастера.
  5. Обновите базу данных на любом мастере в кластере. Выполните команду box.schema.upgrade(). Это обновит системные спейсы Tarantool’а так, чтобы они совпадали с текущей установленной версией Tarantool’а. Изменения распространятся на другие узлы кластера через обычный механизм репликации.

Замечания по поводу некоторых операционных систем

Mac OS

Администрирование экземпляров Tarantool’а на Mac OS возможно только с помощью tarantoolctl. Встроенные системные инструменты не поддерживаются.

FreeBSD

Чтобы tarantoolctl и утилиты init.d работали на FreeBSD, используйте пути, отличные от предложенных в разделе Настройка экземпляров Tarantool’а. Используйте /usr/local/etc/tarantool/ вместо /usr/share/tarantool/ и создайте следующие поддиректории:

  • default для хранения настроек tarantoolctl по умолчанию (см. пример ниже),
  • instances.available для хранения всех доступных файлов экземпляра, и
  • instances.enabled для хранения файлов экземпляра, которые необходимо запускать автоматически с помощью sysvinit.

Так выглядят настройки tarantoolctl по умолчанию на FreeBSD:

default_cfg = {
      pid_file   = "/var/run/tarantool", -- /var/run/tarantool/${INSTANCE}.pid
      wal_dir    = "/var/db/tarantool", -- /var/db/tarantool/${INSTANCE}/
      snap_dir   = "/var/db/tarantool", -- /var/db/tarantool/${INSTANCE}
      vinyl_dir = "/var/db/tarantool", -- /var/db/tarantool/${INSTANCE}
      logger     = "/var/log/tarantool", -- /var/log/tarantool/${INSTANCE}.log
      username   = "tarantool",
  }

  -- instances.available - все доступные экземпляры
  -- instances.enabled - экземпляры для автоматического запуска через sysvinit
  instance_dir = "/usr/local/etc/tarantool/instances.available"

Gentoo Linux

В разделе ниже описывается пакет «dev-db/tarantool», установленный из официального оверлея layman (под названием tarantool).

По умолчанию с экземплярами используется директория /etc/tarantool/instances.available, ее можно переопределить в /etc/default/tarantool.

Управление экземплярами Tarantool’а (запуск/остановка/перезагрузка/проверка статуса и т.д.) можно осуществлять с помощью OpenRC. Рассмотрим пример, как создать экземпляр с управлением OpenRC:

$ cd /etc/init.d
  $ ln -s tarantool your_service_name
  $ ln -s /usr/share/tarantool/your_service_name.lua /etc/tarantool/instances.available/your_service_name.lua

Проверяем, что работает:

$ /etc/init.d/your_service_name start
  $ tail -f -n 100 /var/log/tarantool/your_service_name.log

Сообщения об ошибках

Если вы нашли ошибку в Tarantool, вы окажете нам услугу, сообщив о ней.

Пожалуйста, откройте тикет в репозитории Tarantool на GitHub. Рекомендуем включить следующую информацию:

  • Шаги для воспроизведения ошибки с объяснением того, как ошибочное поведение отличается от описанного в документации ожидаемого поведения. Пожалуйста, указывайте как можно более конкретную информацию. Например, вместо «Я не могу получить определенную информацию» лучше написать «box.space.x:delete() не указывает, что именно было удалено».
  • Название и версию вашей операционной системы, название и версию Tarantool и любую информацию об особенностях вашей машины и ее конфигурации.
  • Сопутствующие файлы – такие как трассировка стека или файл журнала Tarantool’а.

Если это запрос новой функции или это затрагивает определенную группу пользователей, не забудьте это указать.

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

Руководство по разрешению проблем

В данном руководстве используется сторонний модуль stat. Для его установки выполните команду:

$ sudo yum install tarantool-stat
 $ # -- ИЛИ --
 $ sudo apt-get install tarantool-stat

Проблема: при выполнении INSERT/UPDATE-запросов возникает ошибка ER_MEMORY_ISSUE

Возможные причины

  • Нехватка памяти (значения параметров arena_used_ratio и quota_used_ratio из box.slab.info() приближаются к 100%).

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

    $ # подключаемся к админ-консоли нужного экземпляра
     $ tarantoolctl enter <instance_name>
     $ # -- ИЛИ --
     $ tarantoolctl connect <URI>
    
    -- запрашиваем значение arena_used_ratio
     tarantool> require('stat').stat()['slab.arena_used_ratio']
    
     -- запрашиваем значение quota_used_ratio
     tarantool> require('stat').stat()['slab.quota_used_ratio']
    

Решение

У вас есть несколько вариантов действий:

  • Зайти в конфигурационный файл Tarantool и увеличить значение параметра box.cfg{memtx_memory} (при наличии свободных ресурсов).

    In versions of Tarantool before 1.10, the server needs to be restarted to change this parameter. The Tarantool server will be unavailable while restarting from .xlog files, unless you restart it using hot standby mode. In the latter case, nearly 100% server availability is guaranteed.

  • Провести очистку базы данных.

  • Проверьте, нет ли проблем с фрагментацией памяти:

    -- запрашиваем значение quota_used_ratio
     tarantool> require('stat').stat()['slab.quota_used_ratio']
    
     -- запрашиваем значение items_used_ratio
     tarantool> require('stat').stat()['slab.items_used_ratio']
    

    При высокой степени фрагментации памяти (значение параметра quota_used_ratio приближается к 100%, items_used_ratio около 50%) рекомендуется перезапустить Tarantool в режиме горячего резервирования hot standby.

Проблема: Tarantool создает большую нагрузку на CPU

Возможные причины

Поток обработки транзакций нагружает ЦП более чем на 60%.

Решение

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

$ # подключаемся к админ-консоли нужного экземпляра
 $ tarantoolctl enter <instance_name>
 $ # -- ИЛИ --
 $ tarantoolctl connect <URI>
-- запрашиваем RPS для вызовов хранимых процедур
 tarantool> require('stat').stat()['stat.op.call.rps']

Критическое значение RPS – 75 000, в случае большого Lua-приложения (модульного приложения, содержащего более 200 строк кода) – 10 000 - 20 000.

-- запрашиваем RPS для запросов указанного типа
 tarantool> require('stat').stat()['stat.op.<query_type>.rps']

Критическое значение RPS для запросов типа SELECT/INSERT/UPDATE/DELETE – 100 000.

Если основная нагрузка генерируется SELECT-запросами, следует добавить slave-сервер и часть запросов обрабатывать на нем.

Если же нагрузка по большей части приходится на INSERT/UPDATE/DELETE-запросы, рекомендуется провести шардинг базы данных.

Проблема: обработка запросов прекращается по таймауту

Возможные причины

Примечание

Все описанные ниже ситуации можно распознать по записям в журнале Tarantool, начинающимся со слов 'Too long...'.

  1. Быстрые и медленные запросы обрабатываются в одном подключении, что приводит к забиванию readahead-буфера медленными запросами.

    Решение

    У вас есть несколько вариантов действий:

    • Увеличить размер readahead-буфера (box.cfg{readahead}).

      Перезапускать Tarantool при этом не требуется. Для обновления конфигурации необходимо подключиться к Tarantool с помощью утилиты tarantoolctl и передать в box.cfg{} новое значение параметра readahead:

      $ # подключаемся к админ-консоли нужного экземпляра
       $ tarantoolctl enter <instance_name>
       $ # -- ИЛИ --
       $ tarantoolctl connect <URI>
      
      -- задаем новое значение readahead
       tarantool> box.cfg{readahead = 10 * 1024 * 1024}
      

      Пример расчета: при 1000 RPS, размере одного запроса в 1 Кбайт и максимальном времени обработки одного запроса в 10 секунд минимальный размер readahead-буфера должен равняться 10 Мбайт.

    • Обрабатывать быстрые и медленные запросы в отдельных подключениях (решается на уровне бизнес-логики).

  2. Медленная работа дисков.

    Решение

    Проверить занятость дисков (с помощью утилиты iostat, iotop или strace посмотреть на параметр iowait) и попробовать разнести .xlog-файлы и снимки состояния базы данных по разным дискам (т.е. указать разные значения для параметров wal_dir и memtx_dir).

Проблема: параметры репликации lag и idle принимают отрицательные значения

Речь идет о параметрах box.info.replication.(upstream.)lag и box.info.replication.(upstream.)idle из сводной таблицы box.info.replication.

Возможные причины

Не синхронизированы часы на машинах или неправильно работает NTP-сервер.

Решение

Проверить настройки NTP-сервера.

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

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

Речь идет о кластере, состоящем из одного мастера и нескольких реплик. В таком случае значения общих параметров из сводной таблицы box.info.replication, например box.info.replication.lsn, должны приходить с мастера и должны быть одинаковыми на всех репликах. Если такие параметры не совпадают, это свидетельствует о наличии проблем.

Возможные причины

Сбой репликации.

Решение

Перезапустить репликацию.

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

Речь идет о том, что параметр box.info.replication(.upstream).status имеет значение stopped.

Возможные причины

В репликационном кластере, состоящем из двух мастер-серверов, один из серверов попытался выполнить действие, уже выполненное другим сервером, – например, повторно вставить кортеж с таким же уникальным ключом (распознается по ошибке вида 'Duplicate key exists in unique index 'primary' in space <space_name>').

Решение

Возобновить репликацию с помощью следующих команд (должны быть выполнены на всех мастер-серверах):

$ # подключаемся к админ-консоли нужного экземпляра
 $ tarantoolctl enter <instance_name>
 $ # -- ИЛИ --
 $ tarantoolctl connect <URI>
-- перезапускаем репликацию
 tarantool> original_value = box.cfg.replication
 tarantool> box.cfg{replication={}}
 tarantool> box.cfg{replication=original_value}

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

Проблема: Tarantool работает заметно медленнее, чем раньше

Возможные причины

Неэффективное использование памяти (память занята большим количеством неиспользуемых объектов).

Решение

Call the Lua garbage collector with the collectgarbage(„count“) function and measure its execution time with the Tarantool functions clock.bench() or clock.proc().

Пример кода для подсчета потребляемой памяти:

$ # подключаемся к админ-консоли нужного экземпляра
 $ tarantoolctl enter <instance_name>
 $ # -- ИЛИ --
 $ tarantoolctl connect <URI>
-- загрузка модуля clock для работы со временем
 tarantool> local clock = require 'clock'
 -- запускаем таймер
 tarantool> local b = clock.proc()
 -- запускаем сборку мусора
 tarantool> local c = collectgarbage('count')
 -- останавливаем таймер по завершении сборки мусора
 tarantool> return c, clock.proc() - b

Если возвращаемое clock.proc() значение больше 0.001, это может являться признаком неэффективного использования памяти (активного вмешательства не требуется, но рекомендуется оптимизация кода). Если значение превышает 0.01, необходимо провести подробный анализ кода и оптимизировать потребление памяти.

Если значение больше 0,01, код приложения однозначно необходимо проанализировать на предмет оптимизации использования памяти.

Replication

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

Эта глава включает в себя следующие разделы:

Архитектура механизма репликации

Механизм репликации

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

Реплика получает все обновления от мастера, постоянно запрашивая и применяя данные журнала упреждающей записи (WAL). Каждая запись в WAL представляет собой отдельный запрос на изменение данных в Tarantool’е, например, INSERT, UPDATE или DELETE. Такой записи присваивается монотонно возрастающее число, представляющее регистрационный номер в журнале (LSN). По сути, репликация в Tarantool’е является построчной: каждая команда на изменение данных полностью детерминирована и относится к отдельному кортежу. Однако в отличие от типичного построчного журнала, который содержит копии измененных строк полностью, WAL в Tarantool’е включает в себя копии запросов. Например, для запросов типа UPDATE (обновление) Tarantool сохранит только первичный ключ строки и операции обновления для экономии места.

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

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

Data change operations on replication-local spaces (spaces created with is_local = true) are written to the WAL but are not replicated.

Чтобы создать подходящее начальное состояние, к которому можно применить изменения из WAL-файла, для каждого экземпляра из набора реплик должен быть исходный набор файлов контрольной точки – .snap-файлы для memtx и .run-файлы для vinyl. Когда реплика включается в существующий набор реплик, она выбирает существующего мастера и автоматически загружает с него начальное состояние. Это называется начальным включением.

When an entire replica set is bootstrapped for the first time, there is no master which could provide the initial checkpoint. In such a case, replicas connect to each other and elect a master, which then creates the starting set of checkpoint files, and distributes it to all the other replicas. This is called an automatic bootstrap of a replica set.

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

Each replica set is identified by a globally unique identifier, called the replica set UUID. The identifier is created by the master which creates the very first checkpoint, and is part of the checkpoint file. It is stored in system space box.space._schema. For example:

tarantool> box.space._schema:select{'cluster'}
           ---
           - - ['cluster', '6308acb9-9788-42fa-8101-2e0cb9d3c9a0']
           ...

Additionally, each instance in a replica set is assigned its own UUID, when it joins the replica set. It is called an instance UUID and is a globally unique identifier. The instance UUID is checked to ensure that instances do not join a different replica set, e.g. because of a configuration error. A unique instance identifier is also necessary to apply rows originating from different masters only once, that is, to implement multi-master replication. This is why each row in the write ahead log, in addition to its log sequence number, stores the instance identifier of the instance on which it was created. But using a UUID as such an identifier would take too much space in the write ahead log, thus a shorter integer number is assigned to the instance when it joins a replica set. This number is then used to refer to the instance in the write ahead log. It is called instance id. All identifiers are stored in system space box.space._cluster. For example:

tarantool> box.space._cluster:select{}
  ---
  - - [1, '88580b5c-4474-43ab-bd2b-2409a9af80d2']
  ...

Здесь ID экземпляра – 1 (уникальный номер в рамках набора реплик), а UUID экземпляра – 88580b5c-4474-43ab-bd2b-2409a9af80d2 (глобально уникальный).

Using instance IDs is also handy for tracking the state of the entire replica set. For example, box.info.vclock describes the state of replication in regard to each connected peer.

tarantool> box.info.vclock
          ---
          - {1: 827, 2: 584}
          ...

Here vclock contains log sequence numbers (827 and 584) for instances with instance IDs 1 and 2.

Начиная с Tarantool 1.7.7, появилась возможность для администраторов назначать UUID экземпляра и UUID набора реплик вместо сгенерированных системой значений – см. описание конфигурационного параметра replicaset_uuid.

Настройка репликации

Чтобы включить репликацию, необходимо указать два параметра в запросе box.cfg{}:

  • replication which defines the replication source(s), and
  • read_only which is true for a replica and false for a master.

Both these parameters are «dynamic». This allows a replica to become a master and vice versa on the fly with the help of a box.cfg{} request.

Later we will give a detailed example of bootstrapping a replica set.

Роли в репликации: мастер и реплика

The replication role (master or replica) is set by the read_only configuration parameter. The recommended role is «read_only» (replica) for all but one instance in the replica set.

В конфигурации мастер-реплика каждое изменение, сделанное на мастере, будет отображаться на репликах, но не наоборот.

../../../../_images/mr-1m-2r-oneway.svg

Простой набор реплик с двумя экземплярами, один из которых является мастером и расположен на одной машине, а другой – реплика – расположен на другой машине, дает два преимущества:

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

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

../../../../_images/mm-3m-mesh.svg

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

Tarantool multi-master replication guarantees that each change on each master is propagated to all instances and is applied only once. Changes from the same instance are applied in the same order as on the originating instance. Changes from different instances, however, can be mixed and applied in a different order on different instances. This may lead to replication going out of sync in certain cases.

For example, assuming the database is only appended to (i.e. it contains only insertions), a multi-master configuration is safe. If there are also deletions, but it is not mission critical that deletion happens in the same order on all replicas (e.g. the DELETE is used to prune expired data), a master-master configuration is also safe.

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

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

Топологии репликации: каскадная, кольцевая и полная ячеистая

Replication topology is set by the replication configuration parameter. The recommended topology is a full mesh, because it makes potential failover easy.

Некоторые СУБД предлагают топологии каскадной репликации: создание реплики на реплике. Tarantool не рекомендует такие настройки.

../../../../_images/no-cascade.svg

The problem with a cascading replica set is that some instances have no connection to other instances and may not receive changes from them. One essential change that must be propagated across all instances in a replica set is an entry in box.space._cluster system space with the replica set UUID. Without knowing the replica set UUID, a master refuses to accept connections from such instances when replication topology changes. Here is how this can happen:

../../../../_images/cascade-problem-1.svg

У нас есть цепочка из трех экземпляров. Экземпляр №1 содержит записи для экземпляров №1 и №2 в спейсе _cluster. Экземпляры №2 и №3 содержат записи для экземпляров №1, №2 и №3 в своих спейсах _cluster.

../../../../_images/cascade-problem-2.svg

Теперь экземпляр №2 неисправен. Экземпляр №3 пытается подключиться к экземпляру №1, как к новому мастеру, но мастер отклоняет подключение, поскольку не содержит запись для экземпляра №3.

Тем не менее, кольцевая топология поддерживается:

../../../../_images/cascade-to-ring.svg

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

Как бы то ни было, для репликации мастер-мастер рекомендуется полная ячеистая топология:

../../../../_images/mm-3m-mesh.svg

You then can decide where to locate instances of the mesh – within the same data center, or spread across a few data centers. Tarantool will automatically ensure that each row is applied only once on each instance. To remove a degraded instance from a mesh, simply change the replication configuration parameter.

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

Максимальное количество реплик в ячейке – 32.

Настройка набора реплик

Настройка репликации мастер-реплика

Let us first bootstrap a simple master-replica set containing two instances, each located on its own machine. For easier administration, we make the instance files almost identical.

../../../../_images/mr-1m-1r-twoway.png

Ниже пример файла экземпляра для мастера:

-- файл экземпляра для мастера
           box.cfg{
             listen = 3301,
             replication = {'replicator:password@192.168.0.101:3301',  -- URI мастера
                            'replicator:password@192.168.0.102:3301'}, -- URI реплики
             read_only = false
           }
           box.once("schema", function()
              box.schema.user.create('replicator', {password = 'password'})
              box.schema.user.grant('replicator', 'replication') -- настроить роль для репликации
              box.schema.space.create("test")
              box.space.test:create_index("primary")
              print('box.once executed on master')
           end)

где:

  • the box.cfg() listen parameter defines a URI (port 3301 in our example), on which the master can accept connections from replicas.

  • the box.cfg() replication parameter defines the URIs at which all instances in the replica set can accept connections. It includes the replica’s URI as well, although the replica is not a replication source right now.

    Примечание

    For security reasons, we recommend that administrators prevent unauthorized replication sources by associating a password with every user that has a replication role. That way, the URI for replication parameter must have the long form username:password@host:port.

  • the read_only = false parameter setting enables data-change operations on the instance and makes the instance act as a master, not as a replica. That is the only parameter setting in our instance files that will differ.

  • the box.once() function contains database initialization logic that should be executed only once during the replica set lifetime.

In this example, we create a space with a primary index, and a user for replication purposes. We also say print('box.once executed on master') so that it will later be visible on a console whether box.once() was executed.

Примечание

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

Here we use Tarantool’s predefined role named «replication» which by default grants «read» privileges for all database objects («universe»), and we can change privileges for this role as required.

In the replica’s instance file, we set the read_only parameter to «true», and say print('box.once executed on replica') so that later it will be visible that box.once() was not executed more than once. Otherwise the replica’s instance file is identical to the master’s instance file.

-- файл экземпляра для реплики
          box.cfg{
            listen = 3301,
            replication = {'replicator:password@192.168.0.101:3301',  -- URI мастера
                           'replicator:password@192.168.0.102:3301'}, -- URI реплики
            read_only = true
          }
          box.once("schema", function()
             box.schema.user.create('replicator', {password = 'password'})
             box.schema.user.grant('replicator', 'replication') -- настроить роль для репликации
             box.schema.space.create("test")
             box.space.test:create_index("primary")
             print('box.once executed on replica')
          end)

Примечание

The replica does not inherit the master’s configuration parameters, such as those making the checkpoint daemon run on the master. To get the same behavior, set the relevant parameters explicitly so that they are the same on both master and replica.

Теперь можно запустить два экземпляра. Мастер…

$ # запуск мастера
          $ tarantool master.lua
          2017-06-14 14:12:03.847 [18933] main/101/master.lua C> version 1.7.4-52-g980d30092
          2017-06-14 14:12:03.848 [18933] main/101/master.lua C> log level 5
          2017-06-14 14:12:03.849 [18933] main/101/master.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 14:12:03.859 [18933] iproto/101/main I> binary: bound to [::]:3301
          2017-06-14 14:12:03.861 [18933] main/105/applier/replicator@192.168.0. I> can't connect to master
          2017-06-14 14:12:03.861 [18933] main/105/applier/replicator@192.168.0. coio.cc:105 !> SystemError connect, called on fd 14, aka 192.168.0.102:56736: Connection refused
          2017-06-14 14:12:03.861 [18933] main/105/applier/replicator@192.168.0. I> will retry every 1 second
          2017-06-14 14:12:03.861 [18933] main/104/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 14:12:19.878 [18933] main/105/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.102:3301
          2017-06-14 14:12:19.879 [18933] main/101/master.lua I> initializing an empty data directory
          2017-06-14 14:12:19.908 [18933] snapshot/101/main I> saving snapshot `/var/lib/tarantool/master/00000000000000000000.snap.inprogress'
          2017-06-14 14:12:19.914 [18933] snapshot/101/main I> done
          2017-06-14 14:12:19.914 [18933] main/101/master.lua I> vinyl checkpoint done
          2017-06-14 14:12:19.917 [18933] main/101/master.lua I> ready to accept requests
          2017-06-14 14:12:19.918 [18933] main/105/applier/replicator@192.168.0. I> failed to authenticate
          2017-06-14 14:12:19.918 [18933] main/105/applier/replicator@192.168.0. xrow.cc:431 E> ER_LOADING: Instance bootstrap hasn't finished yet
          box.once executed on master
          2017-06-14 14:12:19.920 [18933] main C> entering the event loop

… (the display confirms that box.once() was executed on the master) – and the replica:

$ # запуск реплики
          $ tarantool replica.lua
          2017-06-14 14:12:19.486 [18934] main/101/replica.lua C> version 1.7.4-52-g980d30092
          2017-06-14 14:12:19.486 [18934] main/101/replica.lua C> log level 5
          2017-06-14 14:12:19.487 [18934] main/101/replica.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 14:12:19.494 [18934] iproto/101/main I> binary: bound to [::]:3311
          2017-06-14 14:12:19.495 [18934] main/104/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 14:12:19.495 [18934] main/105/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.102:3302
          2017-06-14 14:12:19.496 [18934] main/104/applier/replicator@192.168.0. I> failed to authenticate
          2017-06-14 14:12:19.496 [18934] main/104/applier/replicator@192.168.0. xrow.cc:431 E> ER_LOADING: Instance bootstrap hasn't finished yet

In both logs, there are messages saying that the replica was bootstrapped from the master:

$ # настройка реплики (из журнала мастера)
          <...>
          2017-06-14 14:12:20.503 [18933] main/106/main I> initial data sent.
          2017-06-14 14:12:20.505 [18933] relay/[::ffff:192.168.0.101]:/101/main I> recover from `/var/lib/tarantool/master/00000000000000000000.xlog'
          2017-06-14 14:12:20.505 [18933] main/106/main I> final data sent.
          2017-06-14 14:12:20.522 [18933] relay/[::ffff:192.168.0.101]:/101/main I> recover from `/Users/e.shebunyaeva/work/tarantool-test-repl/master_dir/00000000000000000000.xlog'
          2017-06-14 14:12:20.922 [18933] main/105/applier/replicator@192.168.0. I> authenticated
$ # настройка реплики (из журнала реплики)
          <...>
          2017-06-14 14:12:20.498 [18934] main/104/applier/replicator@192.168.0. I> authenticated
          2017-06-14 14:12:20.498 [18934] main/101/replica.lua I> bootstrapping replica from 192.168.0.101:3301
          2017-06-14 14:12:20.512 [18934] main/104/applier/replicator@192.168.0. I> initial data received
          2017-06-14 14:12:20.512 [18934] main/104/applier/replicator@192.168.0. I> final data received
          2017-06-14 14:12:20.517 [18934] snapshot/101/main I> saving snapshot `/var/lib/tarantool/replica/00000000000000000005.snap.inprogress'
          2017-06-14 14:12:20.518 [18934] snapshot/101/main I> done
          2017-06-14 14:12:20.519 [18934] main/101/replica.lua I> vinyl checkpoint done
          2017-06-14 14:12:20.520 [18934] main/101/replica.lua I> ready to accept requests
          2017-06-14 14:12:20.520 [18934] main/101/replica.lua I> set 'read_only' configuration option to true
          2017-06-14 14:12:20.520 [18934] main C> entering the event loop

Обратите внимание, что функция box.once() была выполнена только на мастере, хотя мы добавили box.once() в оба файла экземпляра.

Также можно было сначала запустить реплику.

$ # запуск реплики
          $ tarantool replica.lua
          2017-06-14 14:35:36.763 [18952] main/101/replica.lua C> version 1.7.4-52-g980d30092
          2017-06-14 14:35:36.765 [18952] main/101/replica.lua C> log level 5
          2017-06-14 14:35:36.765 [18952] main/101/replica.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 14:35:36.772 [18952] iproto/101/main I> binary: bound to [::]:3301
          2017-06-14 14:35:36.772 [18952] main/104/applier/replicator@192.168.0. I> can't connect to master
          2017-06-14 14:35:36.772 [18952] main/104/applier/replicator@192.168.0. coio.cc:105 !> SystemError connect, called on fd 13, aka 192.168.0.101:56820: Connection refused
          2017-06-14 14:35:36.772 [18952] main/104/applier/replicator@192.168.0. I> will retry every 1 second
          2017-06-14 14:35:36.772 [18952] main/105/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.102:3301

… а затем уже мастера:

$ # запуск мастера
          $ tarantool master.lua
          2017-06-14 14:35:43.701 [18953] main/101/master.lua C> version 1.7.4-52-g980d30092
          2017-06-14 14:35:43.702 [18953] main/101/master.lua C> log level 5
          2017-06-14 14:35:43.702 [18953] main/101/master.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 14:35:43.709 [18953] iproto/101/main I> binary: bound to [::]:3301
          2017-06-14 14:35:43.709 [18953] main/105/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.102:3301
          2017-06-14 14:35:43.709 [18953] main/104/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 14:35:43.709 [18953] main/101/master.lua I> initializing an empty data directory
          2017-06-14 14:35:43.721 [18953] snapshot/101/main I> saving snapshot `/var/lib/tarantool/master/00000000000000000000.snap.inprogress'
          2017-06-14 14:35:43.722 [18953] snapshot/101/main I> done
          2017-06-14 14:35:43.723 [18953] main/101/master.lua I> vinyl checkpoint done
          2017-06-14 14:35:43.723 [18953] main/101/master.lua I> ready to accept requests
          2017-06-14 14:35:43.724 [18953] main/105/applier/replicator@192.168.0. I> failed to authenticate
          2017-06-14 14:35:43.724 [18953] main/105/applier/replicator@192.168.0. xrow.cc:431 E> ER_LOADING: Instance bootstrap hasn't finished yet
          box.once executed on master
          2017-06-14 14:35:43.726 [18953] main C> entering the event loop
          2017-06-14 14:35:43.779 [18953] main/103/main I> initial data sent.
          2017-06-14 14:35:43.780 [18953] relay/[::ffff:192.168.0.101]:/101/main I> recover from `/var/lib/tarantool/master/00000000000000000000.xlog'
          2017-06-14 14:35:43.780 [18953] main/103/main I> final data sent.
          2017-06-14 14:35:43.796 [18953] relay/[::ffff:192.168.0.102]:/101/main I> recover from `/var/lib/tarantool/master/00000000000000000000.xlog'
          2017-06-14 14:35:44.726 [18953] main/105/applier/replicator@192.168.0. I> authenticated

В данном случае реплика ожидает доступности мастера, поэтому порядок запуска не имеет значения. Наша функция box.once() также будет выполняться однократно, только на мастере.

$ # реплика в итоге подключена к мастеру
          $ # и получила настройки (из журнала реплики)
          2017-06-14 14:35:43.777 [18952] main/104/applier/replicator@192.168.0. I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 14:35:43.777 [18952] main/104/applier/replicator@192.168.0. I> authenticated
          2017-06-14 14:35:43.777 [18952] main/101/replica.lua I> bootstrapping replica from 192.168.0.199:3310
          2017-06-14 14:35:43.788 [18952] main/104/applier/replicator@192.168.0. I> initial data received
          2017-06-14 14:35:43.789 [18952] main/104/applier/replicator@192.168.0. I> final data received
          2017-06-14 14:35:43.793 [18952] snapshot/101/main I> saving snapshot `/var/lib/tarantool/replica/00000000000000000005.snap.inprogress'
          2017-06-14 14:35:43.793 [18952] snapshot/101/main I> done
          2017-06-14 14:35:43.795 [18952] main/101/replica.lua I> vinyl checkpoint done
          2017-06-14 14:35:43.795 [18952] main/101/replica.lua I> ready to accept requests
          2017-06-14 14:35:43.795 [18952] main/101/replica.lua I> set 'read_only' configuration option to true
          2017-06-14 14:35:43.795 [18952] main C> entering the event loop

Контролируемое восстановление после сбоя

To perform a controlled failover, that is, swap the roles of the master and replica, all we need to do is to set read_only=true at the master, and read_only=false at the replica. The order of actions is important here. If a system is running in production, we do not want concurrent writes happening both at the replica and the master. Nor do we want the new replica to accept any writes until it has finished fetching all replication data from the old master. To compare replica and master state, we can use box.info.signature.

  1. Настройте read_only=true на мастере.

    # на мастере
              tarantool> box.cfg{read_only=true}
    
  2. Зарегистрируйте текущее состояние мастера с помощью box.info.signature, которое содержит общее количество всех LSN в векторных часах мастера.

    # на мастере
                tarantool> box.info.signature
    
  3. Подождите, пока сигнатура реплики не совпадет с сигнатурой мастера.

    # на реплике
                tarantool> box.info.signature
    
  4. Настройте read_only=false` на реплике, чтобы запустить операции записи данных.

    # на реплике
              tarantool> box.cfg{read_only=false}
    

These four steps ensure that the replica doesn’t accept new writes until it’s done fetching writes from the master.

Настройка репликации мастер-мастер

Now let us bootstrap a two-instance master-master set. For easier administration, we make master#1 and master#2 instance files fully identical.

../../../../_images/mm-2m-mesh.png

Переиспользуем файл экземпляра для мастера из вышеописанного примера мастер-реплика.

-- файл экземпляра для любого из двух мастеров
          box.cfg{
            listen      = 3301,
            replication = {'replicator:password@192.168.0.101:3301',  -- URI мастера 1
                           'replicator:password@192.168.0.102:3301'}, -- URI мастера 2
            read_only   = false
          }
          box.once("schema", function()
             box.schema.user.create('replicator', {password = 'password'})
             box.schema.user.grant('replicator', 'replication') -- настроить роль для репликации
             box.schema.space.create("test")
             box.space.test:create_index("primary")
             print('box.once executed on master #1')
          end)

In the replication parameter, we define the URIs of both masters in the replica set and say print('box.once executed on master #1') so it will be clear when and where the box.once() logic is executed.

Now we can launch the two masters. Again, the launch order doesn’t matter. The box.once() logic will also be executed only once, at the master which is elected as the replica set leader at bootstrap.

$ # запуск мастера №1
          $ tarantool master1.lua
          2017-06-14 15:39:03.062 [47021] main/101/master1.lua C> version 1.7.4-52-g980d30092
          2017-06-14 15:39:03.062 [47021] main/101/master1.lua C> log level 5
          2017-06-14 15:39:03.063 [47021] main/101/master1.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 15:39:03.065 [47021] iproto/101/main I> binary: bound to [::]:3301
          2017-06-14 15:39:03.065 [47021] main/105/applier/replicator@192.168.0.10 I> can't connect to master
          2017-06-14 15:39:03.065 [47021] main/105/applier/replicator@192.168.0.10 coio.cc:107 !> SystemError connect, called on fd 14, aka 192.168.0.102:57110: Connection refused
          2017-06-14 15:39:03.065 [47021] main/105/applier/replicator@192.168.0.10 I> will retry every 1 second
          2017-06-14 15:39:03.065 [47021] main/104/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 15:39:08.070 [47021] main/105/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.102:3301
          2017-06-14 15:39:08.071 [47021] main/105/applier/replicator@192.168.0.10 I> authenticated
          2017-06-14 15:39:08.071 [47021] main/101/master1.lua I> bootstrapping replica from 192.168.0.102:3301
          2017-06-14 15:39:08.073 [47021] main/105/applier/replicator@192.168.0.10 I> initial data received
          2017-06-14 15:39:08.074 [47021] main/105/applier/replicator@192.168.0.10 I> final data received
          2017-06-14 15:39:08.074 [47021] snapshot/101/main I> saving snapshot `/Users/e.shebunyaeva/work/tarantool-test-repl/master1_dir/00000000000000000008.snap.inprogress'
          2017-06-14 15:39:08.074 [47021] snapshot/101/main I> done
          2017-06-14 15:39:08.076 [47021] main/101/master1.lua I> vinyl checkpoint done
          2017-06-14 15:39:08.076 [47021] main/101/master1.lua I> ready to accept requests
          box.once executed on master #1
          2017-06-14 15:39:08.077 [47021] main C> entering the event loop
$ # запуск мастера №2
          $ tarantool master2.lua
          2017-06-14 15:39:07.452 [47022] main/101/master2.lua C> version 1.7.4-52-g980d30092
          2017-06-14 15:39:07.453 [47022] main/101/master2.lua C> log level 5
          2017-06-14 15:39:07.453 [47022] main/101/master2.lua I> mapping 268435456 bytes for tuple arena...
          2017-06-14 15:39:07.455 [47022] iproto/101/main I> binary: bound to [::]:3301
          2017-06-14 15:39:07.455 [47022] main/104/applier/replicator@192.168.0.19 I> remote master is 1.7.4 at 192.168.0.101:3301
          2017-06-14 15:39:07.455 [47022] main/105/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.102:3301
          2017-06-14 15:39:07.455 [47022] main/101/master2.lua I> initializing an empty data directory
          2017-06-14 15:39:07.457 [47022] snapshot/101/main I> saving snapshot `/Users/e.shebunyaeva/work/tarantool-test-repl/master2_dir/00000000000000000000.snap.inprogress'
          2017-06-14 15:39:07.457 [47022] snapshot/101/main I> done
          2017-06-14 15:39:07.458 [47022] main/101/master2.lua I> vinyl checkpoint done
          2017-06-14 15:39:07.459 [47022] main/101/master2.lua I> ready to accept requests
          2017-06-14 15:39:07.460 [47022] main C> entering the event loop
          2017-06-14 15:39:08.072 [47022] main/103/main I> initial data sent.
          2017-06-14 15:39:08.073 [47022] relay/[::ffff:192.168.0.102]:/101/main I> recover from `/Users/e.shebunyaeva/work/tarantool-test-repl/master2_dir/00000000000000000000.xlog'
          2017-06-14 15:39:08.073 [47022] main/103/main I> final data sent.
          2017-06-14 15:39:08.077 [47022] relay/[::ffff:192.168.0.102]:/101/main I> recover from `/Users/e.shebunyaeva/work/tarantool-test-repl/master2_dir/00000000000000000000.xlog'
          2017-06-14 15:39:08.461 [47022] main/104/applier/replicator@192.168.0.10 I> authenticated

Добавление экземпляров

Добавление реплики

../../../../_images/mr-1m-2r-mesh-add.svg

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

-- файл экземпляра для реплики №2
            box.cfg{
              listen = 3301,
              replication = ('replicator:password@192.168.0.101:3301',  -- URI мастера
                             'replicator:password@192.168.0.102:3301',  -- URI реплики№1
                             'replicator:password@192.168.0.103:3301'), -- URI реплики№2
              read_only = true
            }
            box.once("schema", function()
               box.schema.user.create('replicator', {password = 'password'})
               box.schema.user.grant('replicator', 'replication’) -- предоставить роль для репликации
               box.schema.space.create("test")
               box.space.test:create_index("primary")
               print('box.once executed on replica #2')
            end)

Here we add the URI of replica #2 to the replication parameter, so now it contains three URIs.

After we launch the new replica instance, it gets connected to the master instance and retrieves the master’s write-ahead-log and snapshot files:

$ # запуск реплики №2
            $ tarantool replica2.lua
            2017-06-14 14:54:33.927 [46945] main/101/replica2.lua C> version 1.7.4-52-g980d30092
            2017-06-14 14:54:33.927 [46945] main/101/replica2.lua C> log level 5
            2017-06-14 14:54:33.928 [46945] main/101/replica2.lua I> mapping 268435456 bytes for tuple arena...
            2017-06-14 14:54:33.930 [46945] main/104/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.101:3301
            2017-06-14 14:54:33.930 [46945] main/104/applier/replicator@192.168.0.10 I> authenticated
            2017-06-14 14:54:33.930 [46945] main/101/replica2.lua I> bootstrapping replica from 192.168.0.101:3301
            2017-06-14 14:54:33.933 [46945] main/104/applier/replicator@192.168.0.10 I> initial data received
            2017-06-14 14:54:33.933 [46945] main/104/applier/replicator@192.168.0.10 I> final data received
            2017-06-14 14:54:33.934 [46945] snapshot/101/main I> saving snapshot `/var/lib/tarantool/replica2/00000000000000000010.snap.inprogress'
            2017-06-14 14:54:33.934 [46945] snapshot/101/main I> done
            2017-06-14 14:54:33.935 [46945] main/101/replica2.lua I> vinyl checkpoint  done
            2017-06-14 14:54:33.935 [46945] main/101/replica2.lua I> ready to accept requests
            2017-06-14 14:54:33.935 [46945] main/101/replica2.lua I> set 'read_only' configuration option to true
            2017-06-14 14:54:33.936 [46945] main C> entering the event loop

Since we are adding a read-only instance, there is no need to dynamically update the replication parameter on the other running instances. This update would be required if we added a master instance.

However, we recommend specifying the URI of replica #3 in all instance files of the replica set. This will keep all the files consistent with each other and with the current replication topology, and so will help to avoid configuration errors in case of further configuration updates and replica set restart.

Добавление мастера

../../../../_images/mm-3m-mesh-add.svg

Чтобы добавить третьего мастера в набор реплик с конфигурацией мастер-мастер из нашего примера настройки, необходим аналог файлов экземпляров, которые мы создали для настройки других мастеров в этом наборе:

-- файл экземпляра для мастера№3
            box.cfg{
              listen      = 3301,
              replication = {'replicator:password@192.168.0.101:3301',  -- URI мастера №1
                             'replicator:password@192.168.0.102:3301',  -- URI мастера №2
                             'replicator:password@192.168.0.103:3301'}, -- URI мастера №3
              read_only   = true, -- temporarily read-only
            }
            box.once("schema", function()
               box.schema.user.create('replicator', {password = 'password'})
               box.schema.user.grant('replicator', 'replication’) -- предоставить роль для репликации
               box.schema.space.create("test")
               box.space.test:create_index("primary")
            end)

Здесь мы вносим следующие изменения:

  • Add the URI of master #3 to the replication parameter.
  • Временно укажите read_only=true, чтобы отключить операции по изменению данных на этом экземпляре. После запуска мастер №3 будет работать в качестве реплики, пока не получит все данные от других мастеров в наборе реплик.

After we launch master #3, it gets connected to the other master instances and retrieves their write-ahead-log and snapshot files:

$ # запуск мастера №3
            $ tarantool master3.lua
            2017-06-14 17:10:00.556 [47121] main/101/master3.lua C> version 1.7.4-52-g980d30092
            2017-06-14 17:10:00.557 [47121] main/101/master3.lua C> log level 5
            2017-06-14 17:10:00.557 [47121] main/101/master3.lua I> mapping 268435456  bytes for tuple arena...
            2017-06-14 17:10:00.559 [47121] iproto/101/main I> binary: bound to [::]:3301
            2017-06-14 17:10:00.559 [47121] main/104/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.101:3301
            2017-06-14 17:10:00.559 [47121] main/105/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.102:3301
            2017-06-14 17:10:00.559 [47121] main/106/applier/replicator@192.168.0.10 I> remote master is 1.7.4 at 192.168.0.103:3301
            2017-06-14 17:10:00.559 [47121] main/105/applier/replicator@192.168.0.10 I> authenticated
            2017-06-14 17:10:00.559 [47121] main/101/master3.lua I> bootstrapping replica from 192.168.0.102:3301
            2017-06-14 17:10:00.562 [47121] main/105/applier/replicator@192.168.0.10 I> initial data received
            2017-06-14 17:10:00.562 [47121] main/105/applier/replicator@192.168.0.10 I> final data received
            2017-06-14 17:10:00.562 [47121] snapshot/101/main I> saving snapshot `/Users/e.shebunyaeva/work/tarantool-test- repl/master3_dir/00000000000000000009.snap.inprogress'
            2017-06-14 17:10:00.562 [47121] snapshot/101/main I> done
            2017-06-14 17:10:00.564 [47121] main/101/master3.lua I> vinyl checkpoint done
            2017-06-14 17:10:00.564 [47121] main/101/master3.lua I> ready to accept requests
            2017-06-14 17:10:00.565 [47121] main/101/master3.lua I> set 'read_only' configuration option to true
            2017-06-14 17:10:00.565 [47121] main C> entering the event loop
            2017-06-14 17:10:00.565 [47121] main/104/applier/replicator@192.168.0.10 I> authenticated

Next, we add the URI of master #3 to the replication parameter on the existing two masters. Replication-related parameters are dynamic, so we only need to make a box.cfg{} request on each of the running instances:

# добавление URI мастера №3 в источники репликации
          tarantool> box.cfg{replication =
                   > {'replicator:password@192.168.0.101:3301',
                   > 'replicator:password@192.168.0.102:3301',
                   > 'replicator:password@192.168.0.103:3301'}}
          ---
          ...

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

# определение мастера №3 настоящим мастером
            tarantool> box.cfg{read_only=false}
            ---
            ...

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

Orphan status

Starting with Tarantool version 1.9, there is a change to the procedure when an instance joins a replica set. During box.cfg() the instance will try to join all masters listed in box.cfg.replication. If the instance does not succeed with at least the number of masters specified in replication_connect_quorum, then it will switch to orphan status. While an instance is in orphan status, it is read-only.

To «join» a master, a replica instance must «connect» to the master node and then «sync».

«Connect» means contact the master over the physical network and receive acknowledgment. If there is no acknowledgment after box.replication_connect_timeout seconds (usually 4 seconds), and retries fail, then the connect step fails.

«Sync» means receive updates from the master in order to make a local database copy. Syncing is complete when the replica has received all the updates, or at least has received enough updates that the replica’s lag (see replication.upstream.lag in box.info()) is less than or equal to the number of seconds specified in box.cfg.replication_sync_lag. If replication_sync_lag is unset (nil) or set to TIMEOUT_INFINITY, then the replica skips the «sync» state and switches to «follow» immediately.

The following situations are possible.

Situation 1: bootstrap

Here box.cfg{} is being called for the first time. A replica is joining but no replica set exists yet.

  1. Set status to „orphan“.

  2. Try to connect to all nodes from box.cfg.replication, or to the number of nodes required by replication_connect_quorum. Retrying up to 3 times in 30 seconds is possible because this is bootstrap, replication_connect_timeout is overridden.

  3. Abort if not connected to all nodes in box.cfg.replication or replication_connect_quorum.

  4. This instance might be elected as the replica set „leader“. Criteria for electing a leader include vclock value (largest is best), and whether it is read-only or read-write (read-write is best unless there is no other choice). The leader is the master that other instances must join. The leader is the master that executes box_once() functions.

  5. If this instance is elected as the replica set leader, then perform an «automatic bootstrap»:

    1. Set status to „running“.
    2. Return from box.cfg{}.

    Otherwise this instance will be a replica joining an existing replica set, so:

    1. Bootstrap from the leader. See examples in section Bootstrapping a replica set.
    2. In background, sync with all the other nodes in the replication set.

Situation 2: recovery

Here box.cfg{} is not being called for the first time. It is being called again in order to perform recovery.

  1. Perform recovery from the last local snapshot and the WAL files.
  2. Connect to at least replication_connect_quorum nodes.
  3. Sync with all connected nodes, until the difference is not more than replication_sync_lag seconds.

Situation 3: configuration update

Here box.cfg{} is not being called for the first time. It is being called again because some replication parameter or something in the replica set has changed.

  1. Try to connect to all nodes from box.cfg.replication, or to the number of nodes required by replication_connect_quorum, within the time period specified in replication_connect_timeout.
  2. Try to sync with the connected nodes, within the time period specified in replication_sync_timeout.
  3. If earlier steps fail, change status to „orphan“. (Attempts to sync will continue in the background and when/if they succeed then „orphan“ status will end.)
  4. If earlier steps succeed, set status to „running“ (master) or „follow“ (replica).

Situation 4: rebootstrap

Here box.cfg{} is not being called. The replica connected successfully at some point in the past, and is now ready for an update from the master. But the master cannot provide an update. This can happen by accident, or more likely can happen because the replica is slow (its lag is large), and the WAL (.xlog) files containing the updates have been deleted. This is not crippling. The replica can discard what it received earlier, and then ask for the master’s latest snapshot (.snap) file contents. Since it is effectively going through the bootstrap process a second time, this is called «rebootstrapping». However, there has to be one difference from an ordinary bootstrap – the replica’s replica id will remain the same. If it changed, then the master would think that the replica is a new addition to the cluster, and would maintain a record of an instance ID of a replica that has ceased to exist. Rebootstrapping was introduced in Tarantool version 1.10.2 and is completely automatic.

Запуск сервера с репликацией

In addition to the recovery process described in the section Recovery process, the server must take additional steps and precautions if replication is enabled.

Once again the startup procedure is initiated by the box.cfg{} request. One of the box.cfg parameters may be replication which specifies replication source(-s). We will refer to this replica, which is starting up due to box.cfg, as the «local» replica to distinguish it from the other replicas in a replica set, which we will refer to as «distant» replicas.

If there is no snapshot .snap file and the „replication“ parameter is empty:
then the local replica assumes it is an unreplicated «standalone» instance, or is the first replica of a new replica set. It will generate new UUIDs for itself and for the replica set. The replica UUID is stored in the _cluster space; the replica set UUID is stored in the _schema space. Since a snapshot contains all the data in all the spaces, that means the local replica’s snapshot will contain the replica UUID and the replica set UUID. Therefore, when the local replica restarts on later occasions, it will be able to recover these UUIDs when it reads the .snap file.

If there is no snapshot .snap file and the „replication“ parameter is not empty and the „_cluster“ space contains no other replica UUIDs:
then the local replica assumes it is not a standalone instance, but is not yet part of a replica set. It must now join the replica set. It will send its replica UUID to the first distant replica which is listed in replication and which will act as a master. This is called the «join request». When a distant replica receives a join request, it will send back:

  1. UUID набора реплик, в который входит удаленная реплика
  2. содержимое файла снимка .snap удаленной реплики.
    Когда локальная реплика получает эту информацию, она размещает UUID набора реплики в своем спейсе _schema, UUID удаленной реплики и информацию о подключении в своем спейсе _cluster, а затем создает снимок, который содержит все данные, отправленные удаленной репликой. Затем, если в WAL-файлах .xlog локальной реплики содержатся данные, они отправляются на удаленную реплику. Удаленная реплика получается данные и обновляет свою копию данных, а затем добавляет UUID локальной реплики в свой спейс _cluster.

If there is no snapshot .snap file and the „replication“ parameter is not empty and the ``_cluster`` space contains other replica UUIDs:
then the local replica assumes it is not a standalone instance, and is already part of a replica set. It will send its replica UUID and replica set UUID to all the distant replicas which are listed in replication. This is called the «on-connect handshake». When a distant replica receives an on-connect handshake:

  1. удаленная реплика сопоставляет свою версию UUID набора реплик с UUID, переданным в ходе подтверждения связи при подключении. Если они не совпадают, связь не устанавливается, и локальная реплика отобразит ошибку.
  2. удаленная реплика ищет запись о подключающемся экземпляре в своем спейсе _cluster. Если такой записи нет, связь не устанавливается.
    Если есть, связь подтверждается. Удаленная реплика выполняет чтение любой новой информации из своих файлов .snap и .xlog и отправляет новые запросы на локальную реплику.

In the end, the local replica knows what replica set it belongs to, the distant replica knows that the local replica is a member of the replica set, and both replicas have the same database contents.

Если есть файл снимка и указан источник репликаци:
сначала локальная реплика проходит процесс восстановления, описанный в предыдущем разделе, используя свои собственные файлы .snap и .xlog. Затем она отправляет запрос подписки всем репликам в наборе реплик. Запрос подписки содержит векторные часы сервера. Векторные часы включают набор пар „идентификатор сервера, LSN“ для каждой реплики в системном спейсе _cluster. Каждая удаленная реплика, получив запрос подписки, выполняет чтение запросов из файла .xlog и отправляет их на локальную реплику, если LSN из запроса файла .xlog больше, чем LSN векторных часов из запроса подписки. После того, как все реплики из набора реплик отправили ответ на запрос подписки локальной реплики, запуск реплики завершен.

The following temporary limitations applied for Tarantool versions earlier than 1.7.7:

  • URI в параметре replication должны быть указаны в одинаковом порядке на всех репликах. Это необязательно, но помогает соблюдать консистентность.
  • Реплики в наборе реплик должны запускаться не одновременно. Это необязательно, но помогает избежать ситуации, когда все реплики ждут готовности друг друга.

The following limitation still applies for the current Tarantool version:

  • The maximum number of entries in the _cluster space is 32. Tuples for out-of-date replicas are not automatically re-used, so if this 32-replica limit is reached, users may have to reorganize the _cluster space manually.

Удаление экземпляров

To remove an instance from a replica set politely, follow these steps:

  1. Выполните box.cfg{} с пустым источником репликации на экземпляре:

    tarantool> box.cfg{replication=''}
              ---
              ...
    

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

  2. Если экземпляр больше не будет использоваться, удалите записи об экземпляре из следующих мест:

    1. the replication parameter at all running instances in the replica set:

      tarantool> box.cfg{replication=...}
      
    2. the box.space._cluster tuple on any master instance in the replica set. For example, for a record with instance id = 3:

      tarantool> box.space._cluster:select{}
                ---
                - - [1, '913f99c8-aee3-47f2-b414-53ed0ec5bf27']
                  - [2, 'eac1aee7-cfeb-46cc-8503-3f8eb4c7de1e']
                  - [3, '97f2d65f-2e03-4dc8-8df3-2469bd9ce61e']
                ...
                tarantool> box.space._cluster:delete(3)
                ---
                - [3, '97f2d65f-2e03-4dc8-8df3-2469bd9ce61e']
                ...
      

Мониторинг набора реплик

To learn what instances belong in the replica set, and obtain statistics for all these instances, issue a box.info.replication request:

tarantool> box.info.replication
          ---
            replication:
              1:
                id: 1
                uuid: b8a7db60-745f-41b3-bf68-5fcce7a1e019
                lsn: 88
              2:
                id: 2
                uuid: cd3c7da2-a638-4c5d-ae63-e7767c3a6896
                lsn: 31
                upstream:
                  status: follow
                  idle: 43.187747001648
                  peer: replicator@192.168.0.102:3301
                  lag: 0
                downstream:
               vclock: {1: 31}
              3:
                id: 3
                uuid: e38ef895-5804-43b9-81ac-9f2cd872b9c4
                lsn: 54
                upstream:
                  status: follow
                  idle: 43.187621831894
                  peer: replicator@192.168.0.103:3301
                  lag: 2
                downstream:
                  vclock: {1: 54}
          ...

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

../../../../_images/mm-3m-mesh.svg

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

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

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

    A replica sends heartbeat messages to the master every second, and the master is programmed to reconnect automatically if it does not see heartbeat messages within replication_timeout seconds.

    Therefore, in a healthy replication setup, idle should never exceed replication_timeout: if it does, either the replication is lagging seriously behind, because the master is running ahead of the replica, or the network link between the instances is down.

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

    Since the lag calculation uses the operating system clocks from two different machines, do not be surprised if it’s negative: a time drift may lead to the remote master clock being consistently behind the local instance’s clock.

    For multi-master configurations, lag is the maximal lag.

Восстановление после сбоя

«Сбой» – это ситуация, когда мастер становится недоступен вследствие проблем с оборудованием, сетевых неполадок или программной ошибки.

../../../../_images/mr-degraded.svg

В конфигурации мастер-реплика, если мастер пропадает, на репликах выводятся сообщения об ошибке с указанием потери соединения:

$ # сообщения из журнала реплики
          2017-06-14 16:23:10.993 [19153] main/105/applier/replicator@192.168.0. I> can't read row
          2017-06-14 16:23:10.993 [19153] main/105/applier/replicator@192.168.0. coio.cc:349 !> SystemError
          unexpected EOF when reading from socket, called on fd 17, aka 192.168.0.101:57815,
          peer of 192.168.0.101:3301: Broken pipe
          2017-06-14 16:23:10.993 [19153] main/105/applier/replicator@192.168.0. I> will retry every 1 second
          2017-06-14 16:23:10.993 [19153] relay/[::ffff:192.168.0.101]:/101/main I> the replica has closed its socket, exiting
          2017-06-14 16:23:10.993 [19153] relay/[::ffff:192.168.0.101]:/101/main C> exiting the relay loop

… а статус мастера выводится как «отключенный»:

# отчет от реплики №1
            tarantool> box.info.replication
            ---
            - 1:
                id: 1
                uuid: 70e8e9dc-e38d-4046-99e5-d25419267229
                lsn: 542
                upstream:
                  peer: replicator@192.168.0.101:3301
                  lag: 0.00026607513427734
                  status: disconnected
                  idle: 182.36929893494
                  message: connect, called on fd 13, aka 192.168.0.101:58244
              2:
                id: 2
                uuid: fb252ac7-5c34-4459-84d0-54d248b8c87e
                lsn: 0
              3:
                id: 3
                uuid: fd7681d8-255f-4237-b8bb-c4fb9d99024d
                lsn: 0
                downstream:
                  vclock: {1: 542}
            ...
# отчет от реплики №2
            tarantool> box.info.replication
            ---
            - 1:
                id: 1
                uuid: 70e8e9dc-e38d-4046-99e5-d25419267229
                lsn: 542
                upstream:
                  peer: replicator@192.168.0.101:3301
                  lag: 0.00027203559875488
                  status: disconnected
                  idle: 186.76988101006
                  message: connect, called on fd 13, aka 192.168.0.101:58253
              2:
                id: 2
                uuid: fb252ac7-5c34-4459-84d0-54d248b8c87e
                lsn: 0
                upstream:
                  status: follow
                  idle: 186.76960110664
                  peer: replicator@192.168.0.102:3301
                  lag: 0.00020599365234375
              3:
                id: 3
                uuid: fd7681d8-255f-4237-b8bb-c4fb9d99024d
                lsn: 0
            ...

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

  1. Убедитесь, что старый мастер окончательно недоступен:
    • измените правила маршрутизации в сети, чтобы больше не отправлять пакеты на мастер, или
    • отключите мастер-экземпляр, если у вас есть доступ к машине, или
    • отключите питание контейнера или машины.
  2. Выполните box.cfg{read_only=false, listen=URI} на реплике и box.cfg{replication=URI} на других репликах в наборе.

Примечание

Если на старом мастере есть обновления, не переданные до выхода старого мастера из строя, примените их вручную на новом мастере с помощью команд tarantoolctl cat и``tarantoolctl play``.

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

Перезагрузка реплики

Если один из файлов формата .xlog/.snap/.run на реплике поврежден или удален, можно «перезагрузить» реплику данными:

  1. Остановите реплику и удалите все локальные файлы базы данных (с расширениями .xlog/.snap/.run/.inprogress).

  2. Удалите запись о реплике из следующих мест:

    1. the replication parameter at all running instances in the replica set.
    2. the box.space._cluster tuple on the master instance.

    Для получения подробной информации см. Раздел Удаление экземпляров.

  3. Перезапустите реплику с тем же файлом экземпляра для повторного подключения к мастеру. Реплика синхронизируется с мастером после получения всех кортежей.

Примечание

Следует отметить, что эта процедура сработает только в том случае, если на мастере есть WAL-файлы.

Предотвращение дублирующихся действий

Tarantool guarantees that every update is applied only once on every replica. However, due to the asynchronous nature of replication, the order of updates is not guaranteed. We now analyze this problem with more details, provide examples of replication going out of sync, and suggest solutions.

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

In a replica set of two masters, suppose master #1 tries to do something that master #2 has already done. For example, try to insert a tuple with the same unique key:

tarantool> box.space.tester:insert{1, 'data'}

This would cause an error saying Duplicate key exists in unique index 'primary' in space 'tester' and the replication would be stopped. (This is the behavior when the replication_skip_conflict configuration parameter has its default recommended value, false.)

$ # сообщения об ошибках от мастера №1
          2017-06-26 21:17:03.233 [30444] main/104/applier/rep_user@100.96.166.1 I> can't read row
          2017-06-26 21:17:03.233 [30444] main/104/applier/rep_user@100.96.166.1 memtx_hash.cc:226 E> ER_TUPLE_FOUND:
          Duplicate key exists in unique index 'primary' in space 'tester'
          2017-06-26 21:17:03.233 [30444] relay/[::ffff:100.96.166.178]/101/main I> the replica has closed its socket, exiting
          2017-06-26 21:17:03.233 [30444] relay/[::ffff:100.96.166.178]/101/main C> exiting the relay loop

          $ # сообщения об ошибках от мастера №2
          2017-06-26 21:17:03.233 [30445] main/104/applier/rep_user@100.96.166.1 I> can't read row
          2017-06-26 21:17:03.233 [30445] main/104/applier/rep_user@100.96.166.1 memtx_hash.cc:226 E> ER_TUPLE_FOUND:
          Duplicate key exists in unique index 'primary' in space 'tester'
          2017-06-26 21:17:03.234 [30445] relay/[::ffff:100.96.166.178]/101/main I> the replica has closed its socket, exiting
          2017-06-26 21:17:03.234 [30445] relay/[::ffff:100.96.166.178]/101/main C> exiting the relay loop

If we check replication statuses with box.info, we will see that replication at master #1 is stopped (1.upstream.status = stopped). Additionally, no data is replicated from that master (section 1.downstream is missing in the report), because the downstream has encountered the same error:

# статусы репликации (отчет от мастера №3)
          tarantool> box.info
          ---
          - version: 1.7.4-52-g980d30092
            id: 3
            ro: false
            vclock: {1: 9, 2: 1000000, 3: 3}
            uptime: 557
            lsn: 3
            vinyl: []
            cluster:
              uuid: 34d13b1a-f851-45bb-8f57-57489d3b3c8b
            pid: 30445
            status: running
            signature: 1000012
            replication:
              1:
                id: 1
                uuid: 7ab6dee7-dc0f-4477-af2b-0e63452573cf
                lsn: 9
                upstream:
                  peer: replicator@192.168.0.101:3301
                  lag: 0.00050592422485352
                  status: stopped
                  idle: 445.8626639843
                  message: Duplicate key exists in unique index 'primary' in space 'tester'
              2:
                id: 2
                uuid: 9afbe2d9-db84-4d05-9a7b-e0cbbf861e28
                lsn: 1000000
                upstream:
                  status: follow
                  idle: 201.99915885925
                  peer: replicator@192.168.0.102:3301
                  lag: 0.0015020370483398
                downstream:
                  vclock: {1: 8, 2: 1000000, 3: 3}
              3:
                id: 3
                uuid: e826a667-eed7-48d5-a290-64299b159571
                lsn: 3
            uuid: e826a667-eed7-48d5-a290-64299b159571
          ...

Когда позднее репликация возобновлена вручную:

# возобновление остановленной репликации (на всех мастерах)
          tarantool> original_value = box.cfg.replication
          tarantool> box.cfg{replication={}}
          tarantool> box.cfg{replication=original_value}

… the faulty row in the write-ahead-log files is skipped.

Рассинхронизация репликации

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

tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})

When this operation is applied on both instances in the replica set:

# на мастере №1
            tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
            # на мастере №2
            tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})

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

  • each master’s row contains the UUID from master #1,
  • each master’s row contains the UUID from master #2,
  • master #1 has the UUID of master #2, and vice versa.

Коммутативные изменения

The cases described in the previous paragraphs represent examples of non-commutative operations, i.e. operations whose result depends on the execution order. On the contrary, for commutative operations, the execution order does not matter.

Рассмотрим, например, следующую команду:

tarantool> box.space.tester:upsert{{1, 0}, {{'+', 2, 1)}

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

Коннекторы

В этой главе описаны API для различных языков программирования.

Протокол

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

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

Вдаваться в тонкости реализации Tarantool-протокола нужно только при разработке нового коннектора для Tarantool’а – см. полное описание бинарного протокола в Tarantool’е в виде аннотированных BNF-диаграмм (Backus-Naur Form). В остальных случаях достаточно взять уже существующий коннектор для нужного вам языка программирования. Такие коннекторы позволяют легко хранить структуры данных из разных языков в формате Tarantool’а.

Пример пакета данных

С помощью API Tarantool’а клиентские программы могут отправлять пакеты с запросами в адрес экземпляра и получать на них ответы. Вот пример для запроса box.space[513]:insert{'A', 'BB'}. Описания компонентов запроса (в виде BNF-диаграмм) вы найдете на странице о бинарном протоколе в Tarantool’е.

Компонент Байт #0 Байт #1 Байт #2 Байт #3
код для вставки 02      
остаток заголовка
число из 2 цифр: ID спейса cd 02 01  
код для кортежа 21      
число из 1 цифры: количество полей = 2 92      
строка из 1 символа: поле[1] a1 41    
строка из 2 символов: поле[2] a2 42 42  

Теперь получившийся пакет можно послать в адрес экземпляра Tarantool’а и затем расшифровать ответ (описания формата пакета ответов и вопросов вы найдете на той же странице о бинарном протоколе в Tarantool’е). Но более простым и верным способом будет вызвать процедуру, которая сформирует готовый пакет с заданными параметрами. Что-то вроде response = tarantool_routine("insert", 513, "A", "B");. Для этого и существуют API для драйверов для Perl, Python, PHP и т.д.

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

В этой главе приводятся примеры того, как можно установить соединение с Tarantool-сервером с помощью коннекторов для языков Perl, PHP, Python, node.js и C. Обратите внимание, что в примерах указаны фиксированные значения, поэтому для корректной работы всех примеров нужно соблюсти следующие условия:

  • экземпляр (Tarantool) запущен на локальной машине (localhost = 127.0.0.1), а прослушивание для него настроено на порту 3301 (box.cfg.listen = '3301'),
  • в базе есть спейс``examples`` с идентификатором 999 (box.space.examples.id = 999), и у него есть первичный индекс, построенный по ключу числового типа (box.space[999].index[0].parts[1].type = "unsigned"),
  • для пользователя „guest“ настроены права на чтение и запись.

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

box.cfg{listen=3301}
            box.schema.space.create('examples',{id=999})
            box.space.examples:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}})
            box.schema.user.grant('guest','read,write','space','examples')
            box.schema.user.grant('guest','read','space','_space')

Perl

Самый используемый драйвер для Perl – tarantool-perl. Он не входит в репозиторий Tarantool’а, его необходимо устанавливать отдельно. Проще всего установить его путем клонирования с GitHub.

Во избежание незначительных предупреждений, которые может выдать система после первой установки tarantool-perl, начните установку с некоторых других модулей, которые использует tarantool-perl, с CPAN, the Comprehensive Perl Archive Network (Всеобъемлющая сеть архивов Perl):

$ sudo cpan install AnyEvent
          $ sudo cpan install Devel::GlobalDestruction

Затем для установки самого tarantool-perl, выполните:

$ git clone https://github.com/tarantool/tarantool-perl.git tarantool-perl
          $ cd tarantool-perl
          $ git submodule init
          $ git submodule update --recursive
          $ perl Makefile.PL
          $ make
          $ sudo make install

Далее приводится пример полноценной программы на языке Perl, которая осуществляет вставку кортежа [99999,'BB'] в спейс space[999] с помощью API для языка Perl. Перед запуском проверьте, что у экземпляра задан порт для прослушивания на localhost:3301, и в базе создан спейс examples, как описано выше. Чтобы запустить программу, сохраните код в файл с именем example.pl и выполните команду perl example.pl. Программа установит соединение, используя определение спейса для этой цели, откроет сокет для соединения с экземпляром по localhost:3301, пошлет запрос space_object:INSERT, а затем – если всё хорошо – закончит работу без каких-либо сообщений. Если Tarantool не запущен на localhost на прослушивание по порту = 3301, то программа выдаст сообщение об ошибке «Connection refused».

#!/usr/bin/perl
            use DR::Tarantool ':constant', 'tarantool';
            use DR::Tarantool ':all';
            use DR::Tarantool::MsgPack::SyncClient;

            my $tnt = DR::Tarantool::MsgPack::SyncClient->connect(
              host    => '127.0.0.1',                      # поиск Tarantool-сервера по адресу localhost
              port    => 3301,                             # на порту 3301
              user    => 'guest',                          # имя пользователя; здесь же можно добавить 'password=>...'

              spaces  => {
                999 => {                                   # определение спейса  space[999] ...
                  name => 'examples',                      # имя спейса space[999] = 'examples'
                  default_type => 'STR',                   # если тип поля в space[999] не задан, то = 'STR'
                  fields => [ {                            # определение полей в спейсе space[999] ...
                      name => 'field1', type => 'NUM' } ], # имя поля space[999].field[1]='field1', тип ='NUM'
                  indexes => {                             # определение индексов спейса space[999] ...
                    0 => {
                      name => 'primary', fields => [ 'field1' ] } } } } );

            $tnt->insert('examples' => [ 99999, 'BB' ]);

Из-за временных ограничений в языке Perl, вместо полей типа „string“ и „unsigned“ в тестовой программе указаны поля типа „STR“ и „NUM“.

В этой программе мы привели пример использования лишь одного запроса. Для полноценной работы с Tarantool’ом обратитесь к документации из репозитория tarantool-perl.

PHP

tarantool-php is the official PHP connector for Tarantool. It is not supplied as part of the Tarantool repository and must be installed separately (see installation instructions in the connector’s README file).

Here is a complete PHP program that inserts [99999,'BB'] into a space named examples via the PHP API.

Before trying to run, check that the server instance is listening at localhost:3301 and that the space examples exists, as described earlier.

To run, paste the code into a file named example.php and say:

$ php -d extension=~/tarantool-php/modules/tarantool.so example.php

The program will open a socket connection with the Tarantool instance at localhost:3301, then send an INSERT request, then – if all is well – print «Insert succeeded».

If the tuple already exists, the program will print «Duplicate key exists in unique index „primary“ in space „examples“».

<?php
$tarantool = new Tarantool('localhost', 3301);

try {
    $tarantool->insert('examples', [99999, 'BB']);
    echo "Insert succeeded\n";
} catch (Exception $e) {
    echo $e->getMessage(), "\n";
}

В этой программе мы привели пример использования лишь одного запроса. Для полноценной работы с Tarantool’ом обратитесь к документации из проекта tarantool-php на GitHub.

Besides, there is another community-driven GitHub project which includes an alternative connector written in pure PHP, an object mapper, a queue and other packages.

Python

Далее приводится пример полноценной программы на языке Python, которая осуществляет вставку [99999,'Value','Value'] в спейс examples с помощью высокоуровневого API для языка Python.

#!/usr/bin/python
          from tarantool import Connection

          c = Connection("127.0.0.1", 3301)
          result = c.insert("examples",(99999,'Value', 'Value'))
          print result

Для подготовки сохраните код в файл с именем example.py и установите коннектор tarantool-python. Для установки коннектора воспользуйтесь либо командой samp:pip install tarantool>0.4 для установки в директорию /usr (потребуются права уровня root), либо командой pip install tarantool>0.4 --user для установки в директорию ~, т.е. в используемую по умолчанию директорию текущего пользователя. Перед запуском проверьте, что у экземпляра задан порт для прослушивания <cfg_basic-listen>`на``localhost:3301`, и в базе создан спейс examples, как описано выше. Чтобы запустить программу, выполните команду python example.py. Программа установит соединение с Tarantool-сервером, пошлет INSERT-запрос и не выбросит никакого исключения, если всё прошло хорошо. Если такой кортеж уже существует, то программа выбросит исключение tarantool.error.DatabaseError: (3, "Duplicate key exists in unique index 'primary' in space 'examples'").

В этой программе мы привели пример использования лишь одного запроса. Для полноценной работы с Tarantool’ом обратитесь к документации из проекта tarantool-python на GitHub. А на странице проекта queue-python на GitHub вы сможете найти примеры использования Python API для работы с очередями сообщений в Tarantool’е.

Node.js

Самый используемый драйвер для node.js – Node Tarantool driver. Он не входит в репозиторий Tarantool’а, его необходимо устанавливать отдельно. Проще всего установить его вместе с npm. Например, на Ubuntu, когда npm уже установлен, установка драйвера будет выглядеть следующим образом:

$ npm install tarantool-driver --global

Далее приводится пример полноценной программы на языке node.js, которая осуществляет вставку кортежа [99999,'BB'] в спейс space[999] с помощью API для языка node.js. Перед запуском проверьте, что у экземпляра задан порт для прослушивания <cfg_basic-listen>`на``localhost:3301`, и в базе создан спейс examples, как описано выше. Чтобы запустить программу, сохраните код в файл с именем example.rs и выполните команду node example.rs. Программа установит соединение, используя определение спейса для этой цели, откроет сокет для соединения с экземпляром по localhost:3301, отправит INSERT-запрос, а затем – если всё хорошо – выдаст сообщение «Insert succeeded». Если Tarantool не запущен на localhost на прослушивание по порту = 3301, то программа выдаст сообщение об ошибке “Connect failed”. Если у пользователя „guest“ нет прав на соединение, программа выдаст сообщение об ошибке «Auth failed». Если запрос вставки по какой-либо причине не сработает, например поскольку такой кортеж уже существует, то программа выдаст сообщение об ошибке «Insert failed».

var TarantoolConnection = require('tarantool-driver');
          var conn = new TarantoolConnection({port: 3301});
          var insertTuple = [99999, "BB"];
          conn.connect().then(function() {
              conn.auth("guest", "").then(function() {
                  conn.insert(999, insertTuple).then(function() {
                      console.log("Insert succeeded");
                      process.exit(0);
              }, function(e) { console.log("Insert failed");  process.exit(1); });
              }, function(e) { console.log("Auth failed");    process.exit(1); });
              }, function(e) { console.log("Connect failed"); process.exit(1); });

В этой программе мы привели пример использования лишь одного запроса. Для полноценной работы с Tarantool’ом обратитесь к документации из репозитория драйвера для node.js.

C#

Самый используемый драйвер для C# – progaudi.tarantool, который раньше назывался tarantool-csharp. Он не входит в репозиторий Tarantool’а, его необходимо устанавливать отдельно. Создатели драйвера рекомендуют кроссплатформенную установку с помощью Nuget.

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

  1. Установите среду .NET Core от Microsoft. Следуйте инструкциям по установке .NET Core.

Примечание

  • Mono не сработает, как не сработает и .Net от xbuild. Только .NET Core поддерживается на Linux и Mac.
  • Сначала прочитайте Условия лицензионного соглашения с Microsoft, поскольку оно не похоже на обычные соглашения для ПО с открытым кодом, и во время установки система выдаст сообщение о том, что ПО может собирать информацию («This software may collect information about you and your use of the software, and send that to Microsoft.»). Несмотря на это, можно определить переменные окружения, чтобы отказаться от участия в сборе телеметрических данных.
  1. Создайте новый консольный проект.

    $ cd ~
              $ mkdir progaudi.tarantool.test
              $ cd progaudi.tarantool.test
              $ dotnet new console
    
  2. Добавьте ссылку на progaudi.tarantool.

    $ dotnet add package progaudi.tarantool
    
  3. Измените код в Program.cs.

    $ cat <<EOT > Program.cs
              using System;
              using System.Threading.Tasks;
              using ProGaudi.Tarantool.Client;
    
              public class HelloWorld
              {
                static public void Main ()
                {
                  Test().GetAwaiter().GetResult();
                }
                static async Task Test()
                {
                  var box = await Box.Connect("127.0.0.1:3301");
                  var schema = box.GetSchema();
                  var space = await schema.GetSpace("examples");
                  await space.Insert((99999, "BB"));
                }
              }
              EOT
    
  4. Соберите и запустите приложение.

    Перед запуском проверьте, что у экземпляра задан порт для прослушивания на``localhost:3301``, и в базе создан спейс examples, как описано выше.

    $ dotnet restore
              $ dotnet run
    

    Программа:

    • установит соединение, используя определение спейса для этой цели,
    • откроет сокет для соединения с экземпляром по localhost:3301,
    • отправит INSERT-запрос, а затем – если всё хорошо – закончит работу без каких-либо сообщений.

    Если Tarantool не запущен на localhost на прослушивание по порту 3301, или у пользователя „guest“ нет прав на соединение, или запрос вставки по какой-либо причине не сработает, то программа выдаст сообщение об ошибке и другую информацию (трассировку стека и т.д.).

В этой программе мы привели пример использования лишь одного запроса. Для полноценной работы с Tarantool’ом с помощью PHP API, пожалуйста, обратитесь к документации из проекта tarantool-php на GitHub.

C

В этом разделе даны два примера использования высокоуровневого API для Tarantool’а и языка C.

Пример 1

Далее приводится пример полноценной программы на языке C, которая осуществляет вставку кортежа [99999,'B'] в спейс examples с помощью высокоуровневого API для языка C.

#include <stdio.h>
            #include <stdlib.h>

            #include <tarantool/tarantool.h>
            #include <tarantool/tnt_net.h>
            #include <tarantool/tnt_opt.h>

            void main() {
               struct tnt_stream *tnt = tnt_net(NULL);          /* См. ниже = НАСТРОЙКА */
               tnt_set(tnt, TNT_OPT_URI, "localhost:3301");
               if (tnt_connect(tnt) < 0) {                      /* См. ниже = СОЕДИНЕНИЕ */
                   printf("Connection refused\n");
                   exit(-1);
               }
               struct tnt_stream *tuple = tnt_object(NULL);     /* См. ниже = СОЗДАНИЕ ЗАПРОСА */
               tnt_object_format(tuple, "[%d%s]", 99999, "B");
               tnt_insert(tnt, 999, tuple);                     /* См. ниже = ОТПРАВКА ЗАПРОСА */
               tnt_flush(tnt);
               struct tnt_reply reply;  tnt_reply_init(&reply); /* См. ниже = ПОЛУЧЕНИЕ ОТВЕТА */
               tnt->read_reply(tnt, &reply);
               if (reply.code != 0) {
                   printf("Insert failed %lu.\n", reply.code);
               }
               tnt_close(tnt);                                  /* См. ниже = ЗАВЕРШЕНИЕ */
               tnt_stream_free(tuple);
               tnt_stream_free(tnt);
            }

Скопируйте исходный код программы в файл с именем example.c и установите коннектор tarantool-c. Вот один из способов установки tarantool-c (под Ubuntu):

$ git clone git://github.com/tarantool/tarantool-c.git ~/tarantool-c
            $ cd ~/tarantool-c
            $ git submodule init
            $ git submodule update
            $ cmake .
            $ make
            $ make install

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

$ # иногда это необходимо:
            $ export LD_LIBRARY_PATH=/usr/local/lib
            $ gcc -o example example.c -ltarantool

Before trying to run, check that a server instance is listening at localhost:3301 and that the space examples exists, as described earlier. To run the program, say ./example. The program will connect to the Tarantool instance, and will send the request. If Tarantool is not running on localhost with listen address = 3301, the program will print “Connection refused”. If the insert fails, the program will print «Insert failed» and an error number (see all error codes in the source file /src/box/errcode.h).

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

НАСТРОЙКА: Настройка начинается с создания потока (tnt_stream).

struct tnt_stream *tnt = tnt_net(NULL);
            tnt_set(tnt, TNT_OPT_URI, "localhost:3301");

В нашей программе поток назван tnt. Перед установкой соединения с потоком tnt нужно задать ряд опций. Самая важная из них – TNT_OPT_URI. Для этой опции указан URI localhost:3301, т.е. адрес, по которому должно быть настроено :ref:`прослушивание <cfg_basic-listen>`на стороне экземпляра Tarantool’а.

Описание функции:

struct tnt_stream *tnt_net(struct tnt_stream *s)
          int tnt_set(struct tnt_stream *s, int option, variant option-value)

СОЕДИНЕНИЕ: Теперь когда мы создали поток с именем tnt и связали его с конкретным URI, наша программа может устанавливать соединение с экземпляром.

if (tnt_connect(tnt) < 0)
               { printf("Connection refused\n"); exit(-1); }

Описание функции:

int tnt_connect(struct tnt_stream \*s)

Попытка соединения может и не удаться по разным причинам, например если Tarantool-сервер не запущен или в URI-строке указан неверный пароль. В случае неудачи функция вернет -1.

СОЗДАНИЕ ЗАПРОСА: В большинстве запросов требуется передавать структурированные данные, например содержимое кортежа.

struct tnt_stream *tuple = tnt_object(NULL);
            tnt_object_format(tuple, "[%d%s]", 99999, "B");

В данной программе мы используем запрос INSERT, а кортеж содержит целое число и строку. Это простой набор значений без каких-либо вложенных структур или массивов. И передаваемые значения мы можем указать самым простым образом – аналогично тому, как это сделано в стандартной C-функции printf(): %d для обозначения целого числа, %s для обозначения строки, затем числовое значение, затем указатель на строковое значение.

Описание функции:

ssize_t tnt_object_format(struct tnt_stream \*s, const char \*fmt, ...)

ОТПРАВКА ЗАПРОСА: Отправка запросов на изменение данных в базе делается аналогично тому, как это делается в Tarantool-библиотеке box.

tnt_insert(tnt, 999, tuple);
            tnt_flush(tnt);

В данной программе мы делаем INSERT-запрос. В этом запросе мы передаем поток tnt, который ранее использовали для установки соединения, и поток tuple, который также ранее настроили с помощью функции tnt_object_format().

Описание функции:

ssize_t tnt_insert(struct tnt_stream \*s, uint32_t space, struct tnt_stream \*tuple)
            ssize_t tnt_replace(struct tnt_stream \*s, uint32_t space, struct tnt_stream \*tuple)
            ssize_t tnt_select(struct tnt_stream \*s, uint32_t space, uint32_t index,
                               uint32_t limit, uint32_t offset, uint8_t iterator,
                               struct tnt_stream \*key)
            ssize_t tnt_update(struct tnt_stream \*s, uint32_t space, uint32_t index,
                               struct tnt_stream \*key, struct tnt_stream \*ops)

ПОЛУЧЕНИЕ ОТВЕТА: На большинство запросов клиент получает ответ, который содержит информацию о том, был ли данный запрос успешно выполнен, а также содержит набор кортежей.

struct tnt_reply reply;  tnt_reply_init(&reply);
            tnt->read_reply(tnt, &reply);
            if (reply.code != 0)
               { printf("Insert failed %lu.\n", reply.code); }

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

Описание функции:

struct tnt_reply \*tnt_reply_init(struct tnt_reply \*r)
            tnt->read_reply(struct tnt_stream \*s, struct tnt_reply \*r)
            void tnt_reply_free(struct tnt_reply \*r)

ЗАВЕРШЕНИЕ: По окончании сессии нам нужно закрыть соединение, созданное с помощью функции tnt_connect(), и удалить объекты, созданные на этапе настройки.

tnt_close(tnt);
            tnt_stream_free(tuple);
            tnt_stream_free(tnt);

Описание функции:

void tnt_close(struct tnt_stream \*s)
            void tnt_stream_free(struct tnt_stream \*s)

Пример 2

Далее приводится еще один пример полноценной программы на языке C, которая осуществляет выборку по индекс-ключу [99999] из спейса examples с помощью высокоуровневого Tarantool API для языка C. Для вывода результатов в этой программе используются функции из библиотеки MsgPuck. Эти функции нужны для декодирования массивов значений в формате MessagePack.

#include <stdio.h>
            #include <stdlib.h>
            #include <tarantool/tarantool.h>
            #include <tarantool/tnt_net.h>
            #include <tarantool/tnt_opt.h>

            #define MP_SOURCE 1
            #include <msgpuck.h>

            void main() {
                struct tnt_stream *tnt = tnt_net(NULL);
                tnt_set(tnt, TNT_OPT_URI, "localhost:3301");
                if (tnt_connect(tnt) < 0) {
                    printf("Connection refused\n");
                    exit(1);
                }
                struct tnt_stream *tuple = tnt_object(NULL);
                tnt_object_format(tuple, "[%d]", 99999); /* кортеж tuple = ключ для  поиска */
                tnt_select(tnt, 999, 0, (2^32) - 1, 0, 0, tuple);
                tnt_flush(tnt);
                struct tnt_reply reply; tnt_reply_init(&reply);
                tnt->read_reply(tnt, &reply);
                if (reply.code != 0) {
                    printf("Select failed.\n");
                    exit(1);
                }
                char field_type;
                field_type = mp_typeof(*reply.data);
                if (field_type != MP_ARRAY) {
                    printf("no tuple array\n");
                    exit(1);
                }
                long unsigned int row_count;
                uint32_t tuple_count = mp_decode_array(&reply.data);
                printf("tuple count=%u\n", tuple_count);
                unsigned int i, j;
                for (i = 0; i < tuple_count; ++i) {
                    field_type = mp_typeof(*reply.data);
                    if (field_type != MP_ARRAY) {
                        printf("no field array\n");
                        exit(1);
                    }
                    uint32_t field_count = mp_decode_array(&reply.data);
                    printf("  field count=%u\n", field_count);
                    for (j = 0; j < field_count; ++j) {
                        field_type = mp_typeof(*reply.data);
                        if (field_type == MP_UINT) {
                            uint64_t num_value = mp_decode_uint(&reply.data);
                            printf("    value=%lu.\n", num_value);
                        } else if (field_type == MP_STR) {
                            const char *str_value;
                            uint32_t str_value_length;
                            str_value = mp_decode_str(&reply.data, &str_value_length);
                            printf("    value=%.*s.\n", str_value_length, str_value);
                        } else {
                            printf("wrong field type\n");
                            exit(1);
                        }
                    }
                }
                tnt_close(tnt);
                tnt_stream_free(tuple);
                tnt_stream_free(tnt);
            }

Аналогично первому примеру, сохраните исходный код программы в файле с именем example2.c.

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

$ gcc -o example2 example2.c -ltarantool

Для запуска программы выполните команду ./example2.

В этих двух программах мы привели пример использования лишь двух запросов. Для полноценной работы с Tarantool’ом с помощью C API, пожалуйста, обратитесь к документации из проекта tarantool-c на GitHub.

Интерпретация возвращаемых значений

При работе с любым Tarantool-коннектором функции, вызванные с помощью Tarantool’а, возвращают значения в формате MsgPack. Если функция была вызвана через API коннектора, то формат возвращаемых значений будет следующим: скалярные значения возвращаются в виде кортежей (сначала идет идентификатор типа из формата MsgPack, а затем идет значение); все прочие (не скалярные) значения возвращаются в виде групп кортежей (сначала идет идентификатор массива в формате MsgPack, а затем идут скалярные значения). Но если функция была вызвана в рамках бинарного протокола (с помощью команды eval), а не через API коннектора, то подобных изменений формата возвращаемых значений не происходит.

Далее приводится пример создания Lua-функции. Поскольку эту функцию будет вызывать внешний пользователь „guest“ user, то нужно настроить права на исполнение с помощью grant. Эта функция возвращает пустой массив, строку-скаляр, два логических значения и короткое целое число. Значение будут теми же, что описаны в разделе про MsgPack в таблице Стандартные типы в MsgPack-кодировке.

tarantool> box.cfg{listen=3301}
            2016-03-03 18:45:52.802 [27381] main/101/interactive I> ready to accept requests
            ---
            ...
            tarantool> function f() return {},'a',false,true,127; end
            ---
            ...
            tarantool> box.schema.func.create('f')
            ---
            ...
            tarantool> box.schema.user.grant('guest','execute','function','f')
            ---
            ...

Далее идет пример программы на C, из который мы вызываем эту Lua-функцию. Хотя в примере использован код на C, результат будет одинаковым, на каком бы языке ни была написана вызываемая программа: Perl, PHP, Python, Go или Java.

#include <stdio.h>
            #include <stdlib.h>
            #include <tarantool/tarantool.h>
            #include <tarantool/tnt_net.h>
            #include <tarantool/tnt_opt.h>
            void main() {
              struct tnt_stream *tnt = tnt_net(NULL);            /* НАСТРОЙКА */
              tnt_set(tnt, TNT_OPT_URI, "localhost:3301");
               if (tnt_connect(tnt) < 0) {                        /* СОЕДИНЕНИЕ */
                   printf("Connection refused\n");
                   exit(-1);
               }
               struct tnt_stream *tuple = tnt_object(NULL);       /* СОЗДАНИЕ ЗАПРОСА  */
               struct tnt_stream *arg; arg = tnt_object(NULL);
               tnt_object_add_array(arg, 0);
               struct tnt_request *req1 = tnt_request_call(NULL); /* ВЫЗОВ function f() */
               tnt_request_set_funcz(req1, "f");
               tnt_request_set_tuple(req1, arg);
               uint64_t sync1 = tnt_request_compile(tnt, req1);
               tnt_flush(tnt);                                    /* ОТПРАВКА ЗАПРОСА  */
               struct tnt_reply reply;  tnt_reply_init(&reply);   /* ПОЛУЧЕНИЕ ОТВЕТА  */
               tnt->read_reply(tnt, &reply);
               if (reply.code != 0) {
                 printf("Call failed %lu.\n", reply.code);
                 exit(-1);
               }
               const unsigned char *p= (unsigned char*)reply.data;/* ВЫВОД ОТВЕТА */
               while (p < (unsigned char *) reply.data_end)
               {
                 printf("%x ", *p);
                 ++p;
               }
               printf("\n");
               tnt_close(tnt);                                    /* ЗАВЕРШЕНИЕ */
               tnt_stream_free(tuple);
               tnt_stream_free(tnt);
            }

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

dd 0 0 0 5 90 91 a1 61 91 c2 91 c3 91 7f

Первые пять байт – dd 0 0 0 5 – это фрагмент данных в формате MsgPack, означающий «32-битный заголовок массива со значением 5» (см. спецификацию на формат MsgPack). Остальные значения описаны в таблице Стандартные типы в MsgPack-кодировке.

Вопросы и ответы

В:В чем особенности Tarantool’а?
О:Tarantool – представитель нового поколения семейства серверов для in-memory базы данных, разработанный для веб-приложений. Он создан в компании Mail.Ru на основе практического опыта, полученного методом проб и ошибок с начала разработки в 2008 году.
В:Почему Lua?
О:Lua – это легкий, быстрый и расширяемый язык, позволяющий использовать различные парадигмы программирования. Lua также легко встраивается в различные приложения. Сопрограммы (coroutines) в Lua близко соотносятся с файберами (fibers) в Tarantool’е, а вся Lua-архитектура гладко ложится на его внутреннюю реализацию. Lua – это первый язык, на котором можно писать хранимые процедуры для Tarantool’а. В будущем список поддерживаемых языков планируется расширить.
В:В чем ключевое преимущество Tarantool’а?
О:
Tarantool обеспечивает богатый набор функций базы данных (HASH-индексы, TREE-индексы, RTREE-индексы, BITSET-индексы, вторичные индексы, составные индексы, транзакции, триггеры. асинхронная репликация) в гибкой среде Lua-интерпретатора.
Благодаря этим характеристикам, он представляет собой быстрый и надежный in-memory сервер с легким доступом к базе данных, который обрабатывает нетривиальную проблемно-ориентированную логику. Преимущество по сравнению с традиционными SQL-серверами – в производительности: архитектура без блокировок с малой перегрузкой означает, что Tarantool может обслуживать на порядок больше запросов в секунду на аналогичном оборудовании. Преимущество NoSQL-аналогов – в гибкости: Lua допускает гибкую обработку данных, хранимых в компактном денормализированном формате.
В:Кто разрабатывает Tarantool?
О:Во-первых, этим занимается команда разработки в Mail.Ru – см. историю коммитов на github.com/tarantool. Вся разработка ведется открытым образом. Кроме того, активную роль играют члены сообщества разработчиков Tarantool’а. Их силами было создано большинство коннекторов и ведутся доработки под разные дистрибутивы.
В:Возникают ли проблемы из-за того, что Tarantool является in-memory решением?
О:Основной движок баз данных в Tarantool’е работает с оперативной памятью, но при этом он гарантирует сохранность данных благодаря механизму WAL (write ahead log), т.е. журналу упреждающей записи. Также в Tarantool’е используются технологии сжатия и распределения данных, которые позволяют использовать все виды памяти наиболее эффективно. Если Tarantool сталкивается с нехваткой оперативной памяти, то он приостанавливает прием запросов на изменение данных до тех пор, пока не появится свободная память, но при этом с успехом продолжает обработку запросов на чтение и удаление данных. А для больших баз, где объем данных значительно превосходит имеющийся объем оперативной памяти, у Tarantool’а есть второй движок, чьи возможности ограничены лишь размером жесткого диска.
В:Можно ли хранить (большие) объекты BLOB в Tarantool’е?
О:Начиная с Tarantool 1.7, нет «жесткого» ограничения на максимальный размер кортежа. Однако Tarantool предназначен для работы с множеством фрагментов на высокой скорости. Например, при изменении существующего кортежа Tarantool создает новую версию кортежа в памяти. Таким образом, оптимальный размер кортежа – несколько килобайтов.
В:Я удаляю данные из vinyl’а, но использование диска не изменяется. В чем дело?
О:Данные, записываемые в vinyl, сохраняются в исполняемых файлах, обновление которых происходит только путем присоединения новых записей. Такие файлы нельзя изменить, а для удаления маркер удаления (удаленная запись) записывается в новый исполняемый файл. Для уплотнения данных новый и старый исполняемые файлы объединяются, и создается новый исполняемый файл. Независимо от этого, менеджер контрольных точек следит за всеми исполняемыми файлами в контрольной точке и удаляет устаревшие файлы, как только в них отпадает необходимость.

Справочники

Справочник по встроенным модулям

В данном справочнике рассматриваются встроенные Lua-модули Tarantool’а.

Примечание

Некоторые функции в данных модулях представляют собой аналоги функций из стандартных Lua-библиотек. Для достижения наилучшего результата мы рекомендуем использовать функции из встроенных модулей Tarantool’а.

Модуль box

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

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

Вложенный модуль box.cfg

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

Введите команду box.cfg без фигурных скобок для просмотра текущей конфигурации, например:

tarantool> box.cfg
 ---
 - checkpoint_count: 2
   too_long_threshold: 0.5
   slab_alloc_factor: 1.1
   memtx_max_tuple_size: 1048576
   background: false
   <...>
 ...

Чтобы установить параметры, введите команду box.cfg{...}, например:

tarantool> box.cfg{listen = 3301}

Если ввести box.cfg{} без параметров, Tarantool применит настройки по умолчанию:

tarantool> box.cfg{}
tarantool> box.cfg -- sorted in the alphabetic order
---
- background                   = false
  checkpoint_count             = 2
  checkpoint_interval          = 3600
  coredump                     = false
  custom_proc_title            = nil
  feedback_enabled             = true
  feedback_host                = 'https://feedback.tarantool.io'
  feedback_interval            = 3600
  force_recovery               = false
  hot_standby                  = false
  io_collect_interval          = nil
  listen                       = nil
  log                          = nil
  log_format                   = plain
  log_level                    = 5
  log_nonblock                 = true
  memtx_dir                    = '.'
  memtx_max_tuple_size         = 1024 * 1024
  memtx_memory                 = 256 * 1024 *1024
  memtx_min_tuple_size         = 16
  net_msg_max                  = 768
  pid_file                     = nil
  readahead                    = 16320
  read_only                    = false
  replication                  = nil
  replication_connect_timeout  = 4
  replication_skip_conflict    = false
  replication_sync_lag         = 10
  replication_sync_timeout     = 300
  replication_timeout          = 1
  rows_per_wal                 = 500000
  slab_alloc_factor            = 1.05
  snap_io_rate_limit           = nil
  too_long_threshold           = 0.5
  username                     = nil
  vinyl_bloom_fpr              = 0.05
  vinyl_cache                  = 128
  vinyl_dir                    = '.'
  vinyl_max_tuple_size         = 1024 * 1024* 1024 * 1024
  vinyl_memory                 = 128 * 1024 * 1024
  vinyl_page_size              = 8 * 1024
  vinyl_range_size             = 1024 * 1024 * 1024
  vinyl_read_threads           = 1
  vinyl_run_count_per_level    = 2
  vinyl_run_size_ratio         = 3.5
  vinyl_timeout                = 60
  vinyl_write_threads          = 2
  wal_dir                      = '.'
  wal_dir_rescan_delay         = 2
  wal_max_size                 = 256 * 1024 * 1024
  wal_mode                     = 'write'
  worker_pool_threads          = 4
  work_dir                     = nil

Первый вызов box.cfg{...} (с параметрами или без них) запускает модуль базы данных Tarantool’а под названием box. Чтобы выполнить любые операции с базой данных, необходимо сначала вызвать box.cfg{...}.

Команда box.cfg{...} также перезагружает файлы с данными длительного хранения в оперативную память при перезапуске после получения данных.

Вложенный модуль box.ctl

Вложенный модуль box.ctl включает в себя две функции: wait_ro (дождаться режима только для чтения) и wait_rw (дождаться режима чтения и записи). Эти функции используются во время инициализации сервера.

Для box_once() есть особое предназначение. Например, при инициализации реплика может вызвать функцию box.once(), пока сервер все еще находится в режиме только для чтения, и не сможет применить изменения однократно до окончательной инициализации реплики. Это может привести к конфликту между мастером и репликой, если мастер находится в режиме чтения и записи, а реплика доступна только для чтения. Ожидание условия «read only mode = false» (режим только для чтения отключен) решает эту проблему.

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

box.ctl.wait_ro([timeout])

Дождаться, пока не будет выполнено box.info.ro.

Параметры:
  • timeout (number) – максимальное количество секунд ожидания
возвращается:

нулевое значение nil или ошибка (ошибки могут возникать из-за превышения времени ожидания или прерывания работы файбера)

Пример:

tarantool> box.info().ro
 ---
 - false
 ...

 tarantool> n = box.ctl.wait_ro(0.1)
 ---
 - error: timed out
 ...
box.ctl.wait_rw([timeout])

Дождаться, пока не перестанет соблюдаться box.info.ro.

Параметры:
  • timeout (number) – максимальное количество секунд ожидания
возвращается:

нулевое значение nil или ошибка (ошибки могут возникать из-за превышения времени ожидания или прерывания работы файбера)

Пример:

tarantool> box.ctl.wait_rw(0.1)
            ---
            ...

Вложенный модуль box.index

Общие сведения

Вложенный модуль box.index обеспечивает доступ к схемам индекса и ключам индекса в режиме только для чтения. Индексы хранятся в массиве box.space.имя-спейса.index в каждом спейсе. Они предоставляют API для упорядоченной итерации по кортежам. Этот API представляет собой прямую привязку к соответствующим методам объектов типа``box.index`` в движке базы данных.

Индекс

Ниже приведен перечень всех функций и элементов модуля box.index.

Имя Использование
index_object.unique Флаг, если индекс уникальный – true
index_object.type Тип индекса
index_object.parts Массив полей с ключами индекса
index_object:pairs() Подготовка к итерации
index_object:select() Выбор одного или более кортежей по индексу
index_object:get() Выбор кортежа по индексу
index_object:min() Поиск минимального значения в индексе
index_object:max() Поиск максимального значения в индексе
index_object:random() Поиск случайного значения в индексе
index_object:count() Подсчет кортежей с совпадающим значением ключа
index_object:update() Обновление кортежа
index_object:delete() Удаление кортежа по ключу
index_object:alter() Изменение индекса
index_object:drop() Удаление индекса
index_object:rename() Переименование индекса
index_object:bsize() Подсчет байтов для индекса
index_object:stat() Get statistics for an index
index_object:compact() Remove unused index space
index_object:user_defined() Any function / method that any user wants to add
object index_object
index_object.unique

Если индекс уникальный – true, если индекс не уникален – false.

тип возвращаемого значения:
 boolean (логический)
index_object.type

Тип индекса: „TREE“ или „HASH“ или „BITSET“ или „RTREE“.

index_object.parts

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

тип возвращаемого значения:
 таблица

Пример:

tarantool> box.space.tester.index.primary
            ---
            - unique: true
              parts:
              - type: unsigned
                is_nullable: false
                fieldno: 1
              id: 0
              space_id: 513
              name: primary
              type: TREE
            ...
index_object:pairs([key[, iterator-type]])

Поиск кортежа или набора кортежей по заданному индексу и итерация по одному кортежу за раз.

Параметр key (ключ) задает, что именно должно совпадать в индексе.

Примечание

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

Параметр iterator (итератор) задает правило для совпадений и упорядочивания. Различные типы индексов поддерживают различные итераторы. Например, TREE-индекс поддерживает строгий порядок ключей и может вернуть все кортежи в порядке по возрастанию или по убыванию, начиная с указанного ключа. Однако другие типы индексов не поддерживают упорядочивание.

Чтобы понять логику возврата кортежей с помощью итератора, важно знать принципы работы подсистемы обработки транзакций в Tarantool’е. В итераторе Tarantool’а нет собственного постоянного вида просмотра. Наоборот, каждая процедура получает эксклюзивный доступ ко всем кортежам и спейсам до тех пор, пока не «переключится контекст», что может произойти по причине неявной передачи управления или в результате явного вызова функции fiber.yield. Когда поток выполнения возвращается к процедуре, передавшей управление, набор данных может уже значительно измениться. Итерация возобновляется после стадии передачи управления и не сохраняет вид просмотра, а продолжает работу с новым содержимым базы данных. В практическом задании «Индексированный поиск по шаблонам» демонстрируется один из способов одновременного использования итераторов и передачи управления.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значение должно совпасть с индексным ключом, который может быть составным
  • iterator – как определено в таблицах ниже. По умолчанию используется итератор „EQ“
возвращается:

итератор, который может использовать в цикле for/end или с функцией totable()

Возможные ошибки:

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

Факторы сложности Размер индекса, тип индекса; количество кортежей, к которым получен доступ.

Значение искомого ключа может представлять собой число (например, 1234), строку (например, 'abcd') или таблицу из чисел и строк (например, {1234, 'abcd'}). Каждая часть ключа будет сопоставляться с каждой частью ключа в индексе.

Найденные кортежи будут упорядочены по значению ключа в индексе или по хешу значения ключа, если тип индекса – „hash“. Если индекс не уникален, то дубликаты будут упорядочены во вторую очередь по первичному значению ключа. Порядок будет обратным, если тип итератора – „LT“, „LE“ или „REQ“.

Типы итераторов для TREE-индексов

Type Аргументы Описание
box.index.EQ или „EQ“ искомое значение Оператором сравнения будет „==“ (равный). Если ключ индекса равен искомому значению, получим совпадение. Найденные кортежи упорядочены по возрастанию по ключу индекса. Этот тип используется по умолчанию.
box.index.REQ или „REQ“ искомое значение Совпадения находятся таким же образом, что и для box.index.EQ. Найденные кортежи упорядочены по убыванию по ключу индекса.
box.index.GT или „GT“ искомое значение Оператором сравнения будет „>“ (больше чем). Если ключ индекса больше, чем искомое значение, получим совпадение. Найденные кортежи упорядочены по возрастанию по ключу индекса.
box.index.GE или „GE“ искомое значение Оператором сравнения будет „>=“ (больше или равен). Если ключ индекса больше искомого значения или равен ему, получим совпадение. Найденные кортежи упорядочены по возрастанию по ключу индекса.
box.index.ALL или „ALL“ искомое значение Как для box.index.GE.
box.index.LT или „LT“ искомое значение Оператором сравнения будет „<“ (меньше чем). Если ключ индекса меньше искомого значения, получим совпадение. Найденные кортежи упорядочены по убыванию по ключу индекса.
box.index.LE или „LE“ искомое значение Оператором сравнения будет „<=“ (меньше или равен). Если ключ индекса меньше искомого значения или равен ему, получим совпадение. Найденные кортежи упорядочены по убыванию по ключу индекса.

Неофициально можно сказать, что поиск с помощью TREE-индексов пользователи обычно считают интуитивно понятным при условии, что нет нулевых значений и отсутствующих частей. Формально же логика заключается в следующем. Ключ поиска состоит из нуля или более частей, например, {}, {1,2,3},{1,nil,3}. Ключ индекса состоит из одной или более частей, например, {1}, {1,2,3},{1,2,3}. Ключ поиска может содержать нулевое значение nil (но не msgpack.NULL, этот тип не будет правильным). Ключ индекса не может содержать nil или msgpack.NULL, хотя в последующих версиях правила работы Tarantool’а будут другие – поведение поиска с nil может измениться. Возможные итераторы: LT, LE, EQ, REQ, GE, GT. Считается, что ключ поиска соответствует ключу индекса, если следующие операторы, которые представляют собой псевдокод для операции сопоставления, возвращают TRUE.

If (number-of-search-key-parts > number-of-index-key-parts) return ERROR
If (number-of-search-key-parts == 0) return TRUE
for (i = 1; ; ++i)
{
  if (i > number-of-search-key-parts) OR (search-key-part[i] is nil)
  {
    if (iterator is LT or GT) return FALSE
    return TRUE
  }
  if (type of search-key-part[i] is not compatible with type of index-key-part[i])
  {
    return ERROR
  }
  if (search-key-part[i] == index-key-part[i])
  {
    if (iterator is LT or GT) return FALSE
    continue
  }
  if (search-key-part[i] > index-key-part[i])
  {
    if (iterator is EQ or REQ or LE or LT) return FALSE
    return TRUE
  }
  if (search-key-part[i] < index-key-part[i])
  {
    if (iterator is EQ or REQ or GE or GT) return FALSE
    return TRUE
  }
}

Типы итераторов для HASH-индексов

Type Аргументы Описание
box.index.ALL нет Все ключи индекса являются совпадениями. Найденные кортежи упорядочены по возрастанию по хешу ключа индекса, который будет выглядеть случайным.
box.index.EQ или „EQ“ искомое значение Оператором сравнения будет „==“ (равный). Если ключ индекса равен искомому значению, получим совпадение. Количество найденных кортежей будет 0 или 1. Этот тип используется по умолчанию.
box.index.GT или „GT“ искомое значение Оператором сравнения будет „>“ (больше чем). Если хеш ключа индекса больше, чем хеш искомого значения, получим совпадение. Найденные кортежи упорядочены по возрастанию по хешу ключа индекса, который будет выглядеть случайным. При условии, что спейс не обновляется, можно получить все кортежи в спейсе, N кортежей за раз, используя {iterator=“GT“, limit=N} в каждом поиске и последнее найденное значение из предыдущего результата поиска в качестве начального значения для следующего поиска.

Типы итераторов для BITSET-индексов

Type Аргументы Описание
box.index.ALL или „ALL“ нет Все ключи индекса являются совпадениями. Найденные кортежи упорядочены по положению в спейсе.
box.index.EQ или „EQ“ значение bitset (битовое множество) Если ключ индекса равен искомому значению, получим совпадение. Найденные кортежи упорядочены по положению в спейсе. Этот тип используется по умолчанию.
box.index.BITS_ALL_SET значение bitset (битовое множество) Если все биты, которые равны 1 в битовом множестве, также равны 1 в ключе индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.BITS_ANY_SET значение bitset (битовое множество) Если один из битов, которые равны 1 в битовом множестве, также равен 1 в ключе индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.BITS_ALL_NOT_SET значение bitset (битовое множество) Если все биты, которые равны 1 в битовом множестве, равны 0 в ключе индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.

Типы итераторов для RTREE-индексов

Type Аргументы Описание
box.index.ALL или „ALL“ нет Все ключи являются совпадениями. Найденные кортежи упорядочены по положению в спейсе.
box.index.EQ или „EQ“ искомое значение Если все точки прямоугольника-или-параллелепипеда, определенные искомым значением, совпадают с точками прямоугольника-или-параллелепипеда, определенного ключом индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе. «Прямоугольник-или-параллелепипед» означает «прямоугольник-или-параллелепипед, как описано в разделе о RTREE». Этот тип используется по умолчанию.
box.index.GT или „GT“ искомое значение Если все точки прямоугольника-или-параллелепипеда, определенные искомым значением, находятся в пределах прямоугольника-или-параллелепипеда, определенного ключом индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.GE или „GE“ искомое значение Если все точки прямоугольника-или-параллелепипеда, определенные искомым значением, находятся в пределах прямоугольника-или-параллелепипеда, определенного ключом индекса, или рядом с ним, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.LT или „LT“ искомое значение Если все точки прямоугольника-или-параллелепипеда, определенные ключом индекса, находятся в пределах прямоугольника-или-параллелепипеда, определенного искомым значением, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.LE или „LE“ искомое значение Если все точки прямоугольника-или-параллелепипеда, определенные ключом индекса, находятся в пределах прямоугольника-или-параллелепипеда, определенного искомым значением, или рядом с ним, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.OVERLAPS или „OVERLAPS“ искомые значения Если некоторые точки прямоугольника-или-параллелепипеда, определенные искомым значением, находятся в пределах прямоугольника-или-параллелепипеда, определенного ключом индекса, получим совпадение. Найденные кортежи упорядочены по положению в спейсе.
box.index.NEIGHBOR или „NEIGHBOR“ искомое значение Если некоторые точки прямоугольника-или-параллелепипеда, определенные ключом, находятся в пределах, определенных ключом индекса, или рядом с ним, получим совпадение. Найденные кортежи упорядочены следующим образом: сначала ближайший сосед.

Первый пример pairs():

„TREE“-индекс, используемый по умолчанию, и функция pairs():

tarantool> s = box.schema.space.create('space17')
 ---
 ...
 tarantool> s:create_index('primary', {
          >   parts = {1, 'string', 2, 'string'}
          > })
 ---
 ...
 tarantool> s:insert{'C', 'C'}
 ---
 - ['C', 'C']
 ...
 tarantool> s:insert{'B', 'A'}
 ---
 - ['B', 'A']
 ...
 tarantool> s:insert{'C', '!'}
 ---
 - ['C', '!']
 ...
 tarantool> s:insert{'A', 'C'}
 ---
 - ['A', 'C']
 ...
 tarantool> function example()
          >   for _, tuple in
          >     s.index.primary:pairs(nil, {
          >         iterator = box.index.ALL}) do
          >       print(tuple)
          >   end
          > end
 ---
 ...
 tarantool> example()
 ['A', 'C']
 ['B', 'A']
 ['C', '!']
 ['C', 'C']
 ---
 ...
 tarantool> s:drop()
 ---
 ...

Второй пример pairs():

Данный код на Lua найдет все кортежи, значения первичного ключа в которых начинаются с „XY“. Рабочие предположения заключаются в следующем: есть однокомпонентный первичный TREE-индекс по первому полю, которое должно представлять собой строку. Цикл с итератором обеспечивает поиск кортежей, в которых первое значение больше или равно „XY“. Условный оператор в цикле служит для того, чтобы цикл останавливался, если первые две буквы не „XY“.

for _, tuple in
 box.space.t.index.primary:pairs("XY",{iterator = "GE"}) do
   if (string.sub(tuple[1], 1, 2) ~= "XY") then break end
   print(tuple)
 end

Третий пример pairs():

Данный код на Lua найдет все кортежи, значения первичного ключа которых равны или больше 1000 и меньше или равны 1999 (такой тип запроса иногда называют поиском по диапазону или поиском в заданных пределах). Рабочие предположения заключаются в следующем: есть однокомпонентный первичный TREE-индекс по первому полю, которое должно представлять собой число. Цикл с итератором обеспечивает поиск кортежей, в которых первое значение больше или равно 1000. Условный оператор в цикле служит для того, чтобы цикл останавливался, если первое значение больше 1999.

for _, tuple in
 box.space.t2.index.primary:pairs(1000,{iterator = "GE"}) do
   if (tuple[1] > 1999) then break end
   print(tuple)
 end
index_object:select(search-key, options)

Это может быть альтернативой для функции box.space…select(), которая проходит по определенному индексу и может использовать дополнительные параметры, которые определяют тип итератора и пределы (то есть максимальное количество возвращаемых кортежей) и смещение (то есть с какого кортежа в списке начинать).

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
  • options (table/nil) – ни один, любой или все следующие параметры
  • options.iterator – тип итератора
  • options.limit (number) – максимальное количество кортежей
  • options.offset (number) – номер начального кортежа
возвращается:

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

тип возвращаемого значения:
 

массив кортежей

Пример:

-- Создать спейс под названием tester.
 tarantool> sp = box.schema.space.create('tester')
 -- Создать уникальный индекс 'primary'
 -- который не будет нужен для данного примера..
 tarantool> sp:create_index('primary', {parts = {1, 'unsigned' }})
 -- Создать неуникальный индекс 'secondary'
 -- по второму полю.
 tarantool> sp:create_index('secondary', {
          >   type = 'tree',
          >   unique = false,
          >   parts = {2, 'string'}
          > })
 -- Вставить три кортежа, значения в поле2 field[2]
 -- равны 'X', 'Y' и 'Z'.
 tarantool> sp:insert{1, 'X', 'Row with field[2]=X'}
 tarantool> sp:insert{2, 'Y', 'Row with field[2]=Y'}
 tarantool> sp:insert{3, 'Z', 'Row with field[2]=Z'}
 -- Выбрать все кортежи, где вторичные ключи
 -- больше, чем 'X'.`
 tarantool> sp.index.secondary:select({'X'}, {
          >   iterator = 'GT',
          >   limit = 1000
          > })

Результатом будет следующая таблица кортежа:

---
     - - [2, 'Y', 'Row with field[2]=Y']
       - [3, 'Z', 'Row with field[2]=Z']
     ...

Примечание

Параметр index.имя-индекса необязателен. Если он пропущен, то подразумевается первый индекс (первичный ключ). Таким образом, для примера выше, box.space.tester:select({1}, {iterator = 'GT'}) вернет две одинаковых строки по первичному индексу „primary“.

Примечание

Параметр типа итератора iterator = тип-итератора необязателен. Если он пропущен, то подразумевается, что iterator = 'EQ'.

Примечание

Параметр field-value [, значение поля ] необязателен. Если он пропущен, то каждый ключ в индексе будет считаться совпадением независимо от типа итератора. Таким образом, для примера выше, box.space.tester:select{} выберет каждый кортеж в спейсе tester по первому индексу (первичный ключ).

Примечание

box.space.имя-спейса.index.имя-индекса:select(...)[1]` можно заменить box.space.имя-спейса.index.имя-индекса:get(...). А именно, get можно использовать в качестве удобного сокращения для получения первого кортежа в наборе кортежей, который был бы выведен по запросу select. Однако, если в наборе кортежей больше одного кортежа, get вернет ошибку.

Пример с индексом BITSET:

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

tarantool> s = box.schema.space.create('space_with_bitset')
 tarantool> s:create_index('primary_index', {
          >   parts = {1, 'string'},
          >   unique = true,
          >   type = 'TREE'
          > })
 tarantool> s:create_index('bitset_index', {
          >   parts = {2, 'unsigned'},
          >   unique = false,
          >   type = 'BITSET'
          > })
 tarantool> s:insert{'Tuple with bit value = 01', 0x01}
 tarantool> s:insert{'Tuple with bit value = 10', 0x02}
 tarantool> s:insert{'Tuple with bit value = 11', 0x03}
 tarantool> s.index.bitset_index:select(0x02, {
          >   iterator = box.index.EQ
          > })
 ---
 - - ['Tuple with bit value = 10', 2]
 ...
 tarantool> s.index.bitset_index:select(0x02, {
          >   iterator = box.index.BITS_ANY_SET
          > })
 ---
 - - ['Tuple with bit value = 10', 2]
   - ['Tuple with bit value = 11', 3]
 ...
 tarantool> s.index.bitset_index:select(0x02, {
          >   iterator = box.index.BITS_ALL_SET
          > })
 ---
 - - ['Tuple with bit value = 10', 2]
   - ['Tuple with bit value = 11', 3]
 ...
 tarantool> s.index.bitset_index:select(0x02, {
          >   iterator = box.index.BITS_ALL_NOT_SET
          > })
 ---
 - - ['Tuple with bit value = 01', 1]
 ...
index_object:get(key)

Поиск кортежа по заданному индексу, как описано выше.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
возвращается:

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

тип возвращаемого значения:
 

кортеж

Возможные ошибки:

  • отсутствие такого индекса;
  • неправильный тип;
  • больше одного кортежа подходят.

Факторы сложности: Размер индекса, тип индекса. См. также space_object:get().

Пример:

tarantool> box.space.tester.index.primary:get(2)
     ---
     - [2, 'Music']
     ...
index_object:min([key])

Поиск минимального значения в указанном индексе.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
возвращается:

the tuple for the first key in the index. If optional key value is supplied, returns the first key which is greater than or equal to key value. In a future version of Tarantool, index:min(key value) will return nothing if key value is not equal to a value in the index.

тип возвращаемого значения:
 

кортеж

Возможные ошибки: тип индекса не „TREE“.

Факторы сложности: Размер индекса, тип индекса.

Пример:

tarantool> box.space.tester.index.primary:min()
     ---
     - ['Alpha!', 55, 'This is the first tuple!']
     ...
index_object:max([key])

Поиск максимального значения в указанном индексе.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
возвращается:

the tuple for the last key in the index. If optional key value is supplied, returns the last key which is less than or equal to key value. In a future version of Tarantool, index:max(key value) will return nothing if key value is not equal to a value in the index.

тип возвращаемого значения:
 

кортеж

Возможные ошибки: тип индекса не „TREE“.

Факторы сложности: Размер индекса, тип индекса.

Пример:

tarantool> box.space.tester.index.primary:max()
     ---
     - ['Gamma!', 55, 'This is the third tuple!']
     ...
index_object:random(seed)

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

Параметры:
  • index_object (index_object) – ссылка на объект.
  • seed (number) – произвольное неотрицательное целое число
возвращается:

кортеж для случайного ключа в индексе.

тип возвращаемого значения:
 

кортеж

Факторы сложности: Размер индекса, тип индекса.

Примечание про движок базы данных: vinyl не поддерживает random().

Пример:

tarantool> box.space.tester.index.secondary:random(1)
     ---
     - ['Beta!', 66, 'This is the second tuple!']
     ...
index_object:count([key][, iterator])

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

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
  • iterator – метод сопоставления
возвращается:

количество совпадающих ключей индекса.

тип возвращаемого значения:
 

число

Пример:

tarantool> box.space.tester.index.primary:count(999)
 ---
 - 0
 ...
 tarantool> box.space.tester.index.primary:count('Alpha!', { iterator = 'LE' })
 ---
 - 1
 ...
index_object:update(key, {{operator, field_no, value}, ...})

Обновление кортежа.

То же, что и box.space…update(), но поиск ключа происходит в этом индексе, вместо первичного. Данный индекс должен быть уникальным.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
  • operator (string) – тип операции, представленный строкой
  • field_no (number) – к какому полю применяется операция. Номер поля может быть отрицательным, что означает, что позиция рассчитывается с конца кортежа. (#кортеж + отрицательный номер поля + 1)
  • value (lua_value) – какое значение применяется
возвращается:

обновленный кортеж.

тип возвращаемого значения:
 

кортеж

index_object:delete(key)

Удаление кортежа по ключу.

То же, что и box.space…delete(), но поиск ключа происходит в этом индексе, вместо первичного. Данный индекс должен быть уникальным.

Параметры:
  • index_object (index_object) – ссылка на объект.
  • key (scalar/table) – значения для сопоставления с ключом индекса
возвращается:

удаленный кортеж.

тип возвращаемого значения:
 

кортеж

Примечание про движок базы данных: vinyl вернет nil, а не удаленный кортеж.

index_object:alter({options})

Alter an index. It is legal in some circumstances to change an index’s parts and/or change the type and the is_nullable flag for a part. However, this usually causes rebuilding of the space, except for the simple case where the is_nullable flag is changed from false to true.

Параметры:
возвращается:

nil

Возможные ошибки:

  • индекс не существует,
  • the first index cannot be changed to {unique = false}.

Note re storage engine: vinyl supports alter() for non-empty spaces. Primary index definition cannot be altered.

Пример:

tarantool> box.space.space55.index.primary:alter({type = 'HASH'})
     ---
     ...

     tarantool> box.space.vinyl_space.index.i:alter({page_size=4096})
     ---
     ...
index_object:drop()

Удаление индекса. Побочный эффект удаления первичного индекса – все кортежи удалятся.

Параметры:
возвращается:

nil.

Возможные ошибки:

  • индекс не существует,
  • первичный индекс невозможно удалить, если существует вторичный индекс.

Пример:

tarantool> box.space.space55.index.primary:drop()
     ---
     ...
index_object:rename(index-name)

Переименование индекса.

Параметры:
возвращается:

nil

Возможные ошибки: index_object не существует.

Пример:

tarantool> box.space.space55.index.primary:rename('secondary')
     ---
     ...

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

index_object:bsize()

Возврат общего количества байтов, занятых индексом.

Параметры:
возвращается:

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

тип возвращаемого значения:
 

число

index_object:stat()

Return statistics about actions taken that affect the index, including details such as a count of cache evictions, number of accesses, and latency. This is for use with the vinyl engine.

Параметры:
возвращается:

statistics

тип возвращаемого значения:
 

таблица

index_object:compact()

Remove unused index space. For the memtx storage engine this method does nothing; index_object:compact() is only for the vinyl storage engine. For example, with vinyl, if a tuple is deleted, the space is not immediately reclaimed. There is a scheduler for reclaiming space automatically based on the