Top.Mail.Ru
Руководство разработчика | Tarantool
Tarantool
Узнайте содержание релиза 2.8
Tarantool Cartridge Руководство разработчика

Руководство разработчика

Руководство разработчика

Если вы хотите сразу приступить к работе, пропустите подробное описание ниже и переходите к Руководству по началу работы с Cartridge.

Чтобы тщательно изучить, какие возможности Tarantool Cartridge предоставляет для разработки приложений, продолжайте знакомиться с руководством для разработчика по Cartridge.

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

  1. Установить Tarantool Cartridge и другие компоненты среды разработки.
  2. Создать проект.
  3. Разработать приложение. Если это приложение с поддержкой кластеров, реализуйте его логику в виде отдельной (пользовательской) кластерной роли, чтобы инициализировать базу данных в кластерной среде.
  4. Развернуть приложение на сервере или серверах. Это включает в себя настройку и запуск экземпляров.
  5. Если это приложение с поддержкой кластеров, развернуть кластер.

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

  1. Установите cartridge-cli — инструмент командной строки для разработки, развертывания и управления Tarantool-приложениями.
  2. Установите git — систему управления версиями.
  3. Установите npm — менеджер пакетов для node.js.
  4. Установите утилиту unzip.

Чтобы настроить среду разработки, создайте проект по шаблону проекта Tarantool Cartridge. В любой директории выполните:

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

При этом будет автоматически создан Git-репозиторий в новом каталоге /путь/к/<app_name>/ с необходимыми файлами и проставленным тегом версии 0.1.0.

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

Шаблон проекта создает каталог <app_name>/, который включает в себя:

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

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

...
local cartridge = require('cartridge')
...
cartridge.cfg({
-- пример настройки cartridge
 workdir = '/var/lib/tarantool/app',
 advertise_uri = 'localhost:3301',
 cluster_cookie = 'super-cluster-cookie',
 ...
}, {
-- пример настройки модуля box
 memtx_memory = 1000000000,
 ... })
...

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

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

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

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

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

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

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

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

Кластерные роли — это Lua-модули, которые реализуют некоторые конкретные функции и/или логику. Другими словами, кластер Tarantool Cartridge распределяет функции экземпляров на основе ролей.

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

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

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

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

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

    Примечание

    Для получения дополнительной информации о шардировании см. документацию по модулю vshard.

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

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

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

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

  1. Возьмите в качестве примера файл app/roles/custom.lua из проекта. Переименуйте этот файл как угодно, например app/roles/custom-role.lua, и опишите логику роли. Например:

    -- Реализуйте пользовательскую роль в app/roles/custom-role.lua
    local role_name = 'custom-role'
    
    local function init()
    ...
    end
    
    local function stop()
    ...
    end
    
    return {
        role_name = role_name,
        init = init,
        stop = stop,
    }
    

    Здесь значение role_name может отличаться от имени модуля, переданного в функцию cartridge.cfg(). Если не указать переменную role_name, то значением по умолчанию будет имя модуля.

    Примечание

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

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

    -- Зарегистрируйте пользовательскую роль в init.lua,
    ...
    local cartridge = require('cartridge')
    ...
    cartridge.cfg({
        workdir = ...,
        advertise_uri = ...,
        roles = {'custom-role'},
    })
    ...
    

    где custom-role — это название загружаемого Lua-модуля.

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

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

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

    local function init(opts)
        -- Кластер передает Lua-таблицу 'opts', содержащую флаг 'is_master'.
        if opts.is_master then
            local customer = box.schema.space.create('customer',
                { if_not_exists = true }
            )
            customer:format({
                {'customer_id', 'unsigned'},
                {'bucket_id', 'unsigned'},
                {'name', 'string'},
            })
            customer:create_index('customer_id', {
                parts = {'customer_id'},
                if_not_exists = true,
            })
        end
    end
    

    Примечание

    • Спейсами, индексами и форматами не управляют ни vshard-router, ни vshard-storage — это придется делать в рамках пользовательской роли, то есть добавить вызов box.schema.space.create() к первой кластерной роли, как показано в примере выше.
    • Тело функции заключено в условный оператор, который позволяет вызывать функции box только на мастерах. Это предотвращает конфликты репликации, так как данные автоматически передаются на реплики.
  • stop() — это функция завершения работы роли. Ее стоит использовать, если инициализация запускает файбер, который нужно остановить, или же выполняет любую задачу, которую нужно отменить при завершении работы.

  • validate_config() и apply_config() — это функции, которые валидируют и применяют настройки роли соответственно. Их стоит использовать, если какие-то настройки нужно хранить на уровне кластера.

Далее, изучите жизненный цикл ролей, чтобы реализовать необходимые функции.

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

Например:

-- Зависимости для ролей, определенные в app/roles/custom-role.lua
local role_name = 'custom-role'
...
return {
    role_name = role_name,
    dependencies = {'cartridge.roles.vshard-router'},
    ...
}

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

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

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

-- Укажите группы в init.lua
cartridge.cfg({
    vshard_groups = {'hot', 'cold'},
    ...
})

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

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

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

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

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

Если роли не указаны, доступ к статическому роутеру можно получить, как и прежде (когда Tarantool Cartridge не знал о группах):

local vshard = require('vshard')
vshard.router.call(...)

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

local router_role = cartridge.service_get('vshard-router')
local default_router = router_role.get() -- или router_role.get('default')
default_router:call(...)

Кластер отображает все имена пользовательских ролей вместе с именами встроенных ролей из vshard в веб-интерфейсе. Администраторы кластера могут включать и отключать их для определенных экземпляров либо в веб-интерфейсе, либо с помощью общедоступного API. Например:

cartridge.admin.edit_replicaset('replicaset-uuid', {roles = {'vshard-router', 'custom-role'}})

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

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

Кластер вызывает функции роли в следующих случаях:

  • Функция init() обычно выполняется один раз: либо когда администратор включает роль, либо при перезапуске экземпляра. Как правило, достаточно один раз включить роль.
  • Функция stop() – только когда администратор отключает роль, а не во время завершения работы экземпляра.
  • Функция validate_config(): сначала до автоматического вызова box.cfg() (инициализация базы данных), а затем при каждом обновлении конфигурации.
  • Функция apply_config() – при каждом обновлении конфигурации.

В качестве эксперимента дадим кластеру некоторые задачи и посмотрим порядок выполнения функций роли:

  • Присоединение экземпляра или создание набора реплик (в обоих случаях с включенной ролью):
    1. validate_config()
    2. init()
    3. apply_config()
  • Перезапуск экземпляра с включенной ролью:
    1. validate_config()
    2. init()
    3. apply_config()
  • Отключение роли: stop().
  • При вызове cartridge.confapplier.patch_clusterwide():
    1. validate_config()
    2. apply_config()
  • При запущенном восстановлении после отказа:
    1. validate_config()
    2. apply_config()

Учитывая вышеописанное поведение:

  • Функция init() может:
    • Вызывать функции box.
    • Запускать файбер, и в таком случае функция stop() должна позаботиться о завершении работы файбера.
    • Настраивать встроенный HTTP-сервер.
    • Выполнять любой код, связанный с инициализацией роли.
  • Функции stop() должны отменять любую задачу, которую нужно отменить при завершении работы роли.
  • Функция validate_config() должна валидировать любые изменения конфигурации.
  • Функция apply_config() может выполнять любой код, связанный с изменением конфигурации, например, следить за файбером expirationd.

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

Вы можете:

  • Хранить настройки пользовательских ролей в виде разделов в конфигурации на уровне кластера, например:

    # в конфигурационном YAML-файле
    my_role:
      notify_url: "https://localhost:8080"
    
    -- в файле init.lua
    local notify_url = 'http://localhost'
    function my_role.apply_config(conf, opts)
        local conf = conf['my_role'] or {}
        notify_url = conf.notify_url or 'default'
    end
    
  • Загружать и выгружать конфигурацию всего кластера через веб-интерфейс или с помощью API (запросы GET/PUT к конечной точке admin/config: curl localhost:8081/admin/config и curl -X PUT -d "{'my_parameter': 'value'}" localhost:8081/admin/config).

  • Использовать ее в функции apply_config() в своей роли.

Каждый экземпляр в кластере хранит копию конфигурационного файла в своем рабочем каталоге (который можно задать с помощью cartridge.cfg({workdir = ...})):

  • /var/lib/tarantool/<instance_name>/config.yml для экземпляров, развернутых из RPM-пакетов, под управлением systemd.
  • /home/<username>/tarantool_state/var/lib/tarantool/config.yml для экземпляров, развернутых из архивов tar+gz.

Конфигурация кластера представляет собой Lua-таблицу, которую можно загрузить и выгрузить в формате YAML. Если в каждом экземпляре кластера необходимо хранить какие-то данные конфигурации для конкретного приложения (например, схему базы данных, описанную с помощью языка определения данных DDL), можно использовать свой собственный API, добавив в таблицу специальный раздел. Кластер поможет вам безопасно передать настройки всем экземплярам.

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

Самый распространенный способ заключается в том, чтобы:

  • validate_config(conf_new, conf_old) для валидации изменений, сделанных в новой конфигурации (conf_new) по отношению к старой конфигурации (conf_old).
  • apply_config(conf, opts) для выполнения любого кода, связанного с изменениями конфигурации. Входными данными для этой функции будут применяемая конфигурация (conf, которая и есть новая конфигурация, проверенная чуть ранее с помощью validate_config()), а также параметры (аргумент opts включает в себя описываемый ниже логический флаг is_master ).

Важно

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

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

  • Жизненный цикл роли не предполагает, что кластер автоматически вызовет box.cfg() до вызова validate_config().

    Если функция валидации конфигурации вызывает функции из box (например, для проверки формата), убедитесь, что вызовы включены в защитный условный оператор, который проверяет, был ли уже вызов box.cfg():

    -- Внутри функции validate_config():
    if type(box.cfg) == 'table' then
        -- Здесь вы можете вызывать функции из box
    end
    
  • В отличие от функции валидации, apply_config() может свободно вызывать функции из box, потому что кластер применяет пользовательскую конфигурацию после автоматического вызова box.cfg().

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

    По выполнении apply_config(conf, opts) кластер передает флаг is_master в таблице opts, который можно использовать для заключения функций из box в защитный условный оператор, если они могут вызвать конфликт:

    -- Внутри функции  apply_config():
    if opts.is_master then
        -- Здесь вы можете вызывать функции из box
    end
    

Рассмотрим следующий код как часть реализации модуля роли (custom-role.lua):

-- Реализация пользовательской роли

local cartridge = require('cartridge')

local role_name = 'custom-role'

-- Измените конфигурацию, реализовав свой метод, устанавливающий значение поля (как альтернативу для HTTP PUT)
local function set_secret(secret)
    local custom_role_cfg = cartridge.confapplier.get_deepcopy(role_name) or {}
    custom_role_cfg.secret = secret
    cartridge.confapplier.patch_clusterwide({
        [role_name] = custom_role_cfg,
    })
end
-- Валидируйте конфигурацию
local function validate_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    if custom_role_cfg.secret ~= nil then
        assert(type(custom_role_cfg.secret) == 'string', 'custom-role.secret must be a string')
    end
    return true
end
-- Примените её
local function apply_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    local secret = custom_role_cfg.secret or 'default-secret'
    -- И используйте
end

return {
    role_name = role_name,
    set_secret = set_secret,
    validate_config = validate_config,
    apply_config = apply_config,
}

После настройки конфигурации выполните одно из следующих действий:

В примере реализации можно вызвать функцию set_secret(), чтобы применить новую конфигурацию с помощью административной консоли или конечной точки HTTP, если роль ее экспортирует.

Функция set_secret() вызывает cartridge.confapplier.patch_clusterwide(), которая производит двухфазную фиксацию транзакций:

  1. Исправляет активную конфигурацию в памяти: копирует таблицу и заменяет раздел "custom-role" в копии на раздел, который задан функцией set_secret().
  2. Кластер проверяет, можно ли применить новую конфигурацию ко всем экземплярам, кроме отключенных и исключенных. Все обновляемые экземпляры должны быть исправными и в статусе alive в соответствии с требованиями модуля membership.
  3. (Фаза подготовки) Кластер передает исправленную конфигурацию. Каждый экземпляр валидирует ее с помощью функции validate_config() из каждой зарегистрированной роли. В зависимости от результата валидации:
    • В случае успеха (то есть возврата значения true) экземпляр сохраняет новую конфигурацию во временный файл с именем config.prepare.yml в рабочем каталоге.
    • (Фаза отмены) В противном случае экземпляр сообщает об ошибке, а все остальные экземпляры откатывают обновление: удаляют файл, если уже подготовили его.
  4. (Фаза коммита) После успешной подготовки всех экземпляров кластер фиксирует изменения. Каждый экземпляр:
    1. Создает жесткую ссылку на активную конфигурацию.
    2. Атомарно заменяет активный файл конфигурации на подготовленный файл. Атомарная замена неделима: она выполняется или не выполняется полностью — но не частично.
    3. Вызывает функцию apply_config() каждой зарегистрированной роли.

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

Такого рода исправлений можно избежать, если функция validate_config() сможет обнаружить все проблемы конфигурации, которые могут привести к ошибкам в apply_config().

Кластер запускает экземпляр httpd-сервера во время инициализации (cartridge.cfg()). Можно привязать порт к экземпляру через переменную окружения:

-- Получите порт из переменной окружения или используйте значение по умолчанию:
local http_port = os.getenv('HTTP_PORT') or '8080'

local ok, err = cartridge.cfg({
    ...
    -- Передайте порт кластеру:
    http_port = http_port,
    ...
})

Чтобы использовать httpd-экземпляр, получите к нему доступ и настройте маршруты в рамках функции init() для какой-либо роли (например, для роли, которая предоставляет API через HTTP):

local function init(opts)

...

    -- Получите экземпляр httpd:
    local httpd = cartridge.service_get('httpd')
    if httpd ~= nil then
        -- Настройте маршрут, например к метрикам:
        httpd:route({
            method = 'GET',
            path = '/metrics',
            public = true,
        },
        function(req)
            return req:render({json = stat.stat()})
        end
        )
    end
end

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

Чтобы реализовать авторизацию в веб-интерфейсе для каждого экземпляра в кластере Tarantool:

  1. Используйте модуль, к примеру, auth с функцией check_password. Данная функция проверяет учетные данные любого пользователя, который пытается войти в веб-интерфейс.

    Функция check_password принимает имя пользователя и пароль и возвращает результат аутентификации: пройдена или нет.

    -- auth.lua
    
    -- Добавьте функцию для проверки учетных данных
    local function check_password(username, password)
    
        -- Проверьте учетные данные любым удобным вам способом
    
        -- Верните значение, означающее успех или неудачу аутентификации
        if not ok then
            return false
        end
        return true
    end
    ...
    
  2. Передайте имя используемого модуля auth в качестве параметра для cartridge.cfg(), чтобы кластер мог использовать его:

    -- init.lua
    
    local ok, err = cartridge.cfg({
        auth_backend_name = 'auth',
        -- Кластер автоматически вызовет 'require()' для модуля 'auth'.
        ...
    })
    

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

    Примечание

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

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

    -- init.lua
    
    local ok, err = cartridge.cfg({
        auth_backend_name = 'auth',
        auth_enabled = true,
        ...
    })
    

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

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

Например, если версия вашего приложения – 1.2.1, пометьте текущую ветку тегом 1.2.1 (с аннотациями или без них).

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

$ git describe --long --tags
1.2.1-12-g74864f2

Вывод показывает, что после версии 1.2.1 было 12 коммитов. Если мы соберемся упаковать приложение на данном этапе, его полная версия будет 1.2.1-12, а пакет будет называться <имя_приложения>-1.2.1-12.rpm.

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

После упаковки приложения его версия сохраняется в файл VERSION в корневой каталог пакета.

В репозиторий приложения можно добавить файл .cartridge.ignore, чтобы исключить определенные файлы и/или каталоги из сборки пакета.

По большей части логика похожа на логику файлов .gitignore. Основное отличие состоит в том, что в файлах .cartridge.ignore порядок исключения относительно остальных шаблонов не имеет значения, а в файлах .gitignore — имеет.

запись .cartridge.ignore игнорирует все…
target/ папки (поскольку в конце стоит /) под названием target рекурсивно
target файлы или папки под названием target рекурсивно
/target файлы или папки под названием target в самом верхнем каталоге (поскольку в начале стоит /)
/target/ папки под названием target в самом верхнем каталоге (в начале и в конце стоит /)
*.class файлы или папки, оканчивающиеся на .class, рекурсивно
#comment ничего, это комментарий (первый символ – #)
\#comment файлы или папки под названием #comment (\\ для выделения)
target/logs/ папки под названием logs, которые представляют собой подкаталог папки под названием target
target/*/logs/ папки под названием logs на два уровня ниже папки под названием target (* не включает /)
target/**/logs/ папки под названием logs где угодно в пределах папки target (** включает /)
*.py[co] файлы или папки, оканчивающиеся на .pyc или .pyo, но не на .py!
*.py[!co] файлы или папки, оканчивающиеся на что угодно, кроме c или o
*.file[0-9] файлы или папки, оканчивающиеся на цифру
*.file[!0-9] файлы или папки, оканчивающиеся на что угодно, кроме цифры
* всё
/* всё в самом верхнем каталоге (поскольку в начале стоит /)
**/*.tar.gz файлы *.tar.gz или папки, которые находятся на один или несколько уровней ниже исходной папки
!file файлы и папки будут проигнорированы, даже если они подходят под другие типы

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

Назначение экземпляра лидером происходит в зависимости от настроек топологии и конфигурации восстановления после отказа.

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

Когда Cartridge настраивает роли, он учитывает ассоциативный массив лидеров (консолидированный в модуле failover.lua). Ассоциативный массив лидеров составляется, когда экземпляр впервые входит в состояние ConfiguringRoles. В дальнейшем массив обновляется в соответствии с режимом восстановления после отказа.

Каждое изменение в ассоциативном массиве лидеров сопровождается переконфигурацией экземпляра. Когда массив меняется, Cartridge обновляет параметр read_only и вызывает apply_config для каждой роли. Он также устанавливает флаг is_master (который на самом деле означает is_leader, но еще не переименован в силу исторических причин).

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

Логика выбора лидера зависит от режима восстановления после отказа: disabled, eventual или stateful.

Это самый простой случай. Лидером всегда будет первый экземпляр в приоритете восстановления после отказа. Автоматического переключения не будет. Если он отключен, он отключен.

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

Член кластера считается рабочим, если выполняются оба условия:

  1. Он сообщает, что находится в статусе ConfiguringRoles или RolesConfigured;
  2. Его статус по протоколу SWIM: alive или suspect.

Член кластера в статусе suspect становится недоступным (в статусе dead) по истечении failover_timout.

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

  • один роутер «R»,
  • два хранилища: «S1» и «S2».

Тогда можно сказать, что все три экземпляра (R, S1, S2) согласны с тем, что S1 является лидером.

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

Например, вскоре после того, как упал экземпляр S1, экземпляр R уже получил информацию и думает, что S2 — лидер, но S2 еще не получил это сообщение и еще не считает себя лидером. Вот и конфликт.

Аналогично, когда S1 вернется в работу и вновь станет лидером, S2 может не сразу знать об этом. Таким образом, и S1, и S2 будут считать себя лидерами.

Более того, протокол SWIM не совершенен и все еще может передавать ложные сообщения (объявлять, что экземпляр недоступен, когда это не так).

Как и в режиме eventual, каждый экземпляр составляет свой собственный ассоциативный массив лидеров, но теперь массив берется из внешнего поставщика состояния (поэтому этот режим восстановления после отказа называется «с проверкой состояния»). Сейчас поддерживаются два поставщика состояния: etcd и stateboard (изолированный экземпляр Tarantool). Поставщик состояния выступает в качестве хранилища пар ключ-значение (просто replicaset_uuid -> leader_uuid) и механизма блокировки.

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

Все решения принимает координатор — тот, кто захватил блокировку. Координатор реализован как встроенная роль Cartridge. У множества экземпляров может быть включена роль координатора, но только один из них может захватить блокировку. Этот координатор называется «активным».

Блокировка снимается автоматически при закрытии TCP-соединения, или же она может отключиться, если координатор перестанет отвечать (в stateboard это задается опцией --lock_delay, в etcd это часть конфигурации на уровне кластера), поэтому координатор время от времени обновляет блокировку, чтобы считаться рабочим.

Координатор принимает решение на основе SWIM-данных, но алгоритм принятия решения немного отличается от алгоритма в режиме eventual:

  • Сразу после получения блокировки от поставщика состояния, координатор считывает массив лидеров.
  • Если у набора реплик нет лидера, координатор назначает первого лидера в соответствии с приоритетом восстановления после отказа, независимо от статуса SWIM.
  • Если лидер находится в статусе dead, координатор будет принимать решение. Новым лидером станет первый рабочий экземпляр из списка приоритетов восстановления после отказа. Даже если старый лидер вернется в работу, лидер не сменится до тех пор, пока текущий лидер не выйдет из строя. Изменение приоритета восстановления после отказа не повлияет на это.
  • Каждое назначение экземпляра лидером (неважно, назначил он себя сам или получил статус лидера) сохраняется на некоторое время (задается в параметре IMMUNITY_TIMEOUT).

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

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

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

В режимах disabled и eventual продвинуть лидера можно только путем изменения приоритета восстановления после отказа (и применения новой конфигурации на уровне кластера).

В режиме stateful приоритет восстановления после отказа не имеет особого смысла (кроме первого назначения). Вместо этого следует использовать API для продвижения (cartridge.failover_promote в Lua или mutation {cluster{failover_promote()}} в GraphQL), который передает данные о продвижении поставщику состояния.

Режим stateful подразумевает последовательное продвижение: прежде чем разрешить запись, каждый экземпляр выполняет операцию wait_lsn для синхронизации с предыдущим.

Информация о предыдущем лидере (мы называем его vclockkeeper) также хранится на внешнем хранилище. Даже после смещения старого лидера он остается vclockkeeper до тех пор, пока новый лидер успешно ждет и сохраняет vclock на внешнем хранилище.

Если репликация застряла и последовательное продвижение невозможно, у пользователя есть два варианта: отменить продвижение (снова продвинуть старого лидера) или вызвать недопустимое состояние (во всех видах API failover_promote есть флаг для вызова недопустимого состояния force_inconsistency).

Последовательное продвижение не работает для наборов реплик с установленным флагом all_rw и для наборов реплик из одного экземпляра. В этих случаях экземпляр даже не пытается запросить vclockkeeper и выполнить wait_lsn. Но координатор все равно назначает нового лидера, если текущий будет недоступен.

Ни режим eventual, ни режим stateful не защищают набор реплик от появления нескольких лидеров, когда сеть разделена. А фенсинг (изоляция узла, fencing) защищает, обеспечивая соблюдение требования о наличии не более одного лидера в наборе реплик.

Изоляция представляет собой файбер, который время от времени проверяет связь с поставщиком состояния и с репликами. Файбер изоляции работает на экземплярах vclockkeeper; он запускается сразу, когда подтверждается последовательное продвижение. Изоляция не применяется к наборам реплик, которым не нужна консистентность (с одним экземпляром и all_rw).

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

Когда фенсинг срабатывает, он локально фиктивно назначает лидера на nil. Следовательно, экземпляр будет доступен только для чтения. Возврат статуса лидера возможен только при восстановлении кворума; подключение реплики не будет обязательным условием, чтобы вернуть статус лидера. Экземпляр может снова стать лидером в соответствии с правилами последовательного переключения, если только какой-либо другой экземпляр еще не был назначен новым лидером.

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

  • mode: «disabled» / «eventual» / «stateful».
  • state_provider: «tarantool» / «etcd».
  • failover_timeout – время (в секундах) до перевода члена кластера в статусе suspect в статус dead и запуска восстановления после отказа (по умолчанию: 20).
  • tarantool_params: {uri = "...", password = "..."}.
  • etcd2_params: {endpoints = {...}, prefix = "/", lock_delay = 10, username = "", password = ""}.
  • fencing_enabled: true / false (по умолчанию: false).
  • fencing_timeout – время до срабатывания фенсинга после неудачной проверки (по умолчанию: 10).
  • fencing_pause – время на выполнение проверки (по умолчанию: 2).

Должно быть так: failover_timeout > fencing_timeout >= fencing_pause.

Используйте ваш любимый клиент GraphQL (например, Altair) для интроспекции запросов:

  • query {cluster{failover_params{}}},
  • mutation {cluster{failover_params(){}}},
  • mutation {cluster{failover_promote()}}.

Как и другие экземпляры Cartridge, stateboard поддерживает параметры cartridge.argprase:

  • listen
  • workdir
  • password
  • lock_delay

Как и другие параметры argparse, их можно передавать как аргументы командной строки или переменные окружения, например:

.rocks/bin/stateboard --workdir ./dev/stateboard --listen 4401 --password qwerty

Помимо приоритета и режима восстановления после отказа, есть еще несколько закрытых параметров, которые влияют на восстановление после отказа:

  • LONGPOLL_TIMEOUT (failover) – время ожидания длинного запроса (в секундах) для получения данных о назначении лидера (по умолчанию: 30);
  • NETBOX_CALL_TIMEOUT (failover/coordinator) – время ожидания соединения клиента stateboard (в секундах), применяется ко всем соединениям (по умолчанию: 1);
  • RECONNECT_PERIOD (coordinator) – время (в секундах) для повторного соединения с поставщиком состояния, если он недоступен (по умолчанию: 5);
  • IMMUNITY_TIMEOUT (coordinator) – минимальное время (в секундах) до переназначения лидера (по умолчанию: 15).

Cartridge организует кластер — распределенную систему экземпляров Tarantool. Одно из основных понятий — конфигурация на уровне кластера. Каждый экземпляр в кластере хранит свою копию конфигурации.

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

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

Конфигурация экземпляра состоит из двух наборов параметров:

Задать эти параметры можно:

  1. В аргументах в командной строке.
  2. В переменных окружения.
  3. В конфигурационном файле формата YAML.
  4. В файле init.lua.

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

Независимо от того, как вы запускаете экземпляры, необходимо задать следующие параметры cartridge.cfg() для каждого экземпляра:

  • advertise_uri – либо <ХОСТ>:<ПОРТ>, либо <ХОСТ>:, либо <ПОРТ>. Используется другими экземплярами для подключения. НЕ указывайте 0.0.0.0 – это должен быть внешний IP-адрес, а не привязка сокета.
  • http_port – порт, который используется, чтобы открывать административный веб-интерфейс и API. По умолчанию: 8081. Чтобы отключить, укажите "http_enabled": False.
  • workdir — каталог, где хранятся все данные: файлы снимка, журналы упреждающей записи и конфигурационный файл cartridge. По умолчанию: ..

Если вы запустите экземпляры, используя интерфейс командной строки cartridge или systemctl, сохраните конфигурацию в формате YAML, например:

my_app.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
my_app.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
my_app.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}

С помощью интерфейса командной строки cartridge вы можете передать путь к этому файлу в качестве аргумента командной строки --cfg для команды cartridge start – или же указать путь в конфигурации cartridge./.cartridge.yml или ~/.cartridge.yml):

cfg: cartridge.yml
run_dir: tmp/run
apps_path: /usr/local/share/tarantool

С помощью systemctl сохраните файл в формате YAML в /etc/tarantool/conf.d/ (по умолчанию путь systemd) или в место, указанное в переменной окружения TARANTOOL_CFG.

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

$ tarantool init.lua --alias router --memtx-memory 100 --workdir "~/db/3301" --advertise_uri "localhost:3301" --http_port "8080"

В файловой системе конфигурация на уровне кластера показана в виде дерева файлов. В workdir любого сконфигурированного экземпляра можно найти каталог:

config/
├── auth.yml
├── topology.yml
└── vshard_groups.yml

Это конфигурация на уровне кластера с тремя разделами config по умолчанию: auth, topology и vshard_groups.

Исторически сложилось так, что есть два вида кластерной конфигурация:

  • один файл config.yml старого образца, в котором находятся все разделы, и
  • представленное выше современное представление из нескольких файлов.

Так конфигурация выглядела до версии Cartridge 2.0 и до сих используется в таком виде в HTTP API и вспомогательных утилитах luatest:

# config.yml
---
auth: {...}
topology: {...}
vshard_groups: {...}
...

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

В Lua конфигурация представлена в виде объекта ClusterwideConfig (таблица с метаметодами). Для получения более подробной информации обратитесь к документации модуля cartridge.clusterwide-config.

Конфигурация на уровне кластера в Cartridge одинакова для всех экземпляров, что достигается использованием двухфазного алгоритма фиксации транзакций, который реализован в модуле cartridge.twophase. Изменения в конфигурации на уровне кластера подразумевают применение этих изменений к каждому экземпляру в кластере.

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

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

Для получения более подробной информации, обратитесь к справочнику по API cartridge.config_patch_clusterwide.

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

Некоторые сторонние роли (например, sharded-queue и cartridge-extensions также могут использовать такие разделы для конкретных ролей.

Конфигурацию на уровне кластера можно изменять различными способами. Вносить изменения можно с помощью API для Lua, HTTP или GraphQL. Кроме того, есть вспомогательные утилиты luatest.

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

Пример:

cat > config.yml << CONFIG
---
custom_section: {}
...
CONFIG

Загрузка новой конфигурации:

curl -v "localhost:8081/admin/config" -X PUT --data-binary @config.yml

Скачивание конфигурации:

curl -v "localhost:8081/admin/config" -o config.yml

Скачивать можно только разделы для ролей. Системные разделы (topology, auth, vshard_groups, users_acl) нельзя ни загружать, ни скачивать.

Если включена авторизация, следует использовать параметр curl: --user username:password.

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

g.cluster.main_server:graphql({query = [[
    mutation($sections: [ConfigSectionInput!]) {
        cluster {
            config(sections: $sections) {
                filename
                content
            }
        }
    }]],
    variables = {sections = {
      {
        filename = 'custom_section.yml',
        content = '---\n{}\n...',
      }
    }}
})

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

Как и HTTP API, запрос GraphQL cluster {config} не подойдет для управления системными разделами.

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

  • cartridge.config_patch_clusterwide
  • cartridge.config_get_deepcopy
  • cartridge.config_get_readonly

Упрощенный пример (из sharded-queue):

function create_tube(tube_name, tube_opts)
    local tubes = cartridge.config_get_deepcopy('tubes') or {}
    tubes[tube_name] = tube_opts or {}

    return cartridge.config_patch_clusterwide({tubes = tubes})
end

local function validate_config(conf)
    local tubes = conf.tubes or {}
    for tube_name, tube_opts in pairs(tubes) do
        -- валидируйте tube_opts
    end
    return true
end

local function apply_config(conf, opts)
    if opts.is_master then
        local tubes = cfg.tubes or {}
        -- создайте tubes в соответствии с конфигурацией
    end
    return true
end

В Cartridge есть вспомогательные утилиты для тестирования, которые предлагают методы управления настройками:

  • cartridge.test-helpers.cluster:upload_config,
  • cartridge.test-helpers.cluster:download_config.

Они реализованы путем оборачивания HTTP API.

Пример:

g.before_all(function()
    g.cluster = helpers.Cluster.new(...)
    g.cluster:upload_config({some_section = 'some_value'})
    t.assert_equals(
        g.cluster:download_config(),
        {some_section = 'some_value'}
    )
end)

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

Развертывание включает в себя:

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

Развернуть приложение на Tarantool Cartridge можно четырьмя способами:

  • в виде RPM-пакета (для производственной среды);
  • в виде DEB-пакета (для производственной среды);
  • в виде архива tar.gz (для тестирования или как обходной путь для производственной среды, если отсутствует доступ уровня root);
  • из исходных файлов (только для локального тестирования).

Выбор между DEB- и RPM-пакетами зависит от пакетного менеджера целевой ОС. DEB используется для Debian Linux и его производных, а RPM — для CentOS/RHEL и других основанных на RPM дистрибутивов Linux.

Важно

Если при упаковке приложения вы используете Tarantool Community Edition, то эта версия Tarantool будет среди зависимостей приложения.

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

В производственной среде для управления экземплярами приложения и доступа к записям журнала рекомендуется использовать подсистему systemd.

Чтобы развернуть приложение на Tarantool Cartridge, выполните следующие шаги:

  1. Упакуйте приложения в распространяемый пакет:

    $ cartridge pack rpm [APP_PATH] [--use-docker]
    $ # -- ИЛИ --
    $ cartridge pack deb [APP_PATH] [--use-docker]
    

    где

    • APP_PATH — путь к директории приложения. Значение по умолчанию — . (текущая директория).
    • --use-docker — флаг, который нужно использовать при упаковке приложения на различных листрибутивах Linux или на macOS. Он гарантирует что результирующий артефакт содержит совместимые с Linux модули и исполняемые файлы.

    В результате создается RPM- или DEB-пакет со следующим способом именования: <APP_NAME>-<VERSION>.{rpm,deb}. Например, ./my_app-0.1.0-1-g8c57dcb.rpm или ./my_app-0.1.0-1-g8c57dcb.deb. Чтобы больше узнать о формате и использовании команды cartridge pack, прочитайте ее описание.

  2. Загрузите сгенерированный пакет на целевой сервер.

  3. Установите приложение:

    $ sudo yum install <APP_NAME>-<VERSION>.rpm
    $ # -- ИЛИ --
    $ sudo dpkg -i <APP_NAME>-<VERSION>.deb
    
  4. Настройте экземпляры приложения.

    Конфигурация хранится в файле /etc/tarantool/conf.d/instances.yml. Создайте этот файл и укажите параметры экземпляров. Подробности читайте в Конфигурация экземпляров.

    Например:

    my_app:
      cluster_cookie: secret-cookie
    
    my_app.router:
      advertise_uri: localhost:3301
      http_port: 8081
    
    my_app.storage-master:
      advertise_uri: localhost:3302
      http_port: 8082
    
    my_app.storage-replica:
      advertise_uri: localhost:3303
      http_port: 8083
    

    Примечание

    Не указывайте среди этих настроек рабочие директории экземпляров. Используйте для этого переменную окружения TARANTOOL_WORKDIR в юнит-файле для каждого из соответствующих экземпляров (/etc/systemd/system/<APP_NAME>@.service).

  5. Запустите экземпляры приложения с помощью команды systemctl.

    Подробности читайте на странице Запуск и остановка с помощью systemctl.

    $ sudo systemctl start my_app@router
    $ sudo systemctl start my_app@storage-master
    $ sudo systemctl start my_app@storage-replica
    
  6. Если ваше приложение поддерживает кластеры, переходите к развертыванию кластера.

    Примечание

    При переносе приложения из локальной среды тестирования на рабочий сервер на этом этапе можно переиспользовать тестовые настройки:

    1. В веб-интерфейсе кластера тестовой среды нажмите Configuration files > Download, чтобы сохранить тестовую конфигурацию.
    2. В веб-интерфейсе кластера на рабочем сервере нажмите Configuration files > Upload, чтобы загрузить сохраненную конфигурацию.

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

  • systemctl — для остановки, перезапуска, проверки статуса экземпляров и т. д.;
  • journalctl для работы с журналами экземпляров.

Во время установки приложения на Tarantool Cartridge дополнительно создаются следующие сущности:

  • Группа (user group) tarantool.
  • Системный пользователь tarantool. Все экземпляры приложения запускаются от имени этого пользователя. Группа tarantool — основная группа пользователя tarantool. Пользователь создается с параметром -s /sbin/nologin.
  • Директории и файлы, перечисленные в таблице ниже (<APP_NAME> — имя приложения, %i — имя экземпляра):
Путь Права доступа Владелец:Группа Описание
/etc/systemd/system/<APP_NAME>.service -rw-r--r-- root:root юнит-файл systemd для сервиса <APP_NAME>
/etc/systemd/system/<APP_NAME>@.service -rw-r--r-- root:root юнит-файл systemd, позволяющий запускать экземпляры сервиса <APP_NAME>
/usr/share/tarantool/<APP_NAME>/ drwxr-xr-x root:root Директория. Содержит исполняемые файлы приложения.
/etc/tarantool/conf.d/ drwxr-xr-x root:root Директория для YAML-файлов с конфигурацией экземпляров приложений, таких как instances.yml.
/var/lib/tarantool/<APP_NAME>.%i/ drwxr-xr-x tarantool:tarantool Рабочие директории экземпляров приложения. Каждая директория содержит данные экземпляра, а именно: файлы WAL и файлы-снимки, а также файлы конфигурации приложения в формате YAML.
/var/run/tarantool/ drwxr-xr-x tarantool:tarantool Директория. Содержит следующие файлы для каждого экземпляра: <APP_NAME>.%i.pid и <APP_NAME>.%i.control.
/var/run/tarantool/<APP_NAME>.%i.pid -rw-r--r-- tarantool:tarantool Содержит ID процесса.
/var/run/tarantool/<APP_NAME>.%i.control srwxr-xr-x tarantool:tarantool Unix-сокет для подключения к экземпляру с помощью утилиты tarantoolctl.

  1. Упакуйте файлы приложения в распространяемый пакет:

    $ cartridge pack tgz APP_NAME
    

    Будет создан архив tar+gz (например, ./my_app-0.1.0-1.tgz).

  2. Загрузите архив на серверы, на которых установлены tarantool и (необязательно) cartridge-cli.

  3. Распакуйте архив:

    $ tar -xzvf APP_NAME-VERSION.tgz
    
  4. Настройте экземпляр(ы). Создайте файл под названием /etc/tarantool/conf.d/instances.yml. Например:

    my_app:
     cluster_cookie: secret-cookie
    
    my_app.instance-1:
     http_port: 8081
     advertise_uri: localhost:3301
    
    my_app.instance-2:
     http_port: 8082
     advertise_uri: localhost:3302
    

    См. описание здесь.

  5. Запустите экземпляры Tarantool. Это можно сделать, используя:

    • tarantoolctl, например:

      $ tarantool init.lua # запускает один экземпляр
      
    • или cartridge, например:

      $ # в директории приложения
      $ cartridge start # запускает все экземпляры
      $ cartridge start .router_1 # запускает один экземпляр
      
      $ # в среде с несколькими приложениями
      $ cartridge start my_app # запускает все экземпляры приложения my_app
      $ cartridge start my_app.router # запускает один экземпляр
      
  6. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

    При переносе приложения из локальной среды тестирования на рабочий сервер на этом этапе можно переиспользовать тестовые настройки:

    1. В веб-интерфейсе кластера тестовой среды нажмите Configuration files > Download, чтобы сохранить тестовую конфигурацию.
    2. В веб-интерфейсе кластера на рабочем сервере нажмите Configuration files > Upload, чтобы загрузить сохраненную конфигурацию.

Такой метод развертывания предназначен только для локального тестирования.

  1. Вытяните все зависимости в каталог .rocks:

    $ tarantoolctl rocks make
    
  2. Настройте экземпляр(ы). Создайте файл под названием /etc/tarantool/conf.d/instances.yml. Например:

    my_app:
     cluster_cookie: secret-cookie
    
    my_app.instance-1:
     http_port: 8081
     advertise_uri: localhost:3301
    
    my_app.instance-2:
     http_port: 8082
     advertise_uri: localhost:3302
    

    См. описание здесь.

  3. Запустите экземпляры Tarantool. Это можно сделать, используя:

    • tarantoolctl, например:

      $ tarantool init.lua # запускает один экземпляр
      
    • или cartridge, например:

      $ # в директории приложения
      $ cartridge start # запускает все экземпляры
      $ cartridge start .router_1 # запускает один экземпляр
      
      $ # в среде с несколькими приложениями
      $ cartridge start my_app # запускает все экземпляры приложения my_app
      $ cartridge start my_app.router # запускает один экземпляр
      
  4. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

    При переносе приложения из локальной среды тестирования на рабочий сервер на этом этапе можно переиспользовать тестовые настройки:

    1. В веб-интерфейсе кластера тестовой среды нажмите Configuration files > Download, чтобы сохранить тестовую конфигурацию.
    2. В веб-интерфейсе кластера на рабочем сервере нажмите Configuration files > Upload, чтобы загрузить сохраненную конфигурацию.

В зависимости от способа развертывания вы можете запускать/останавливать экземпляры, используя tarantool, интерфейс командной строки cartridge или systemctl.

С помощью tarantool можно запустить только один экземпляр:

# простейшая команда
$ tarantool init.lua

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

Чтобы остановить экземпляр, используйте Ctrl+C.

С помощью интерфейса командной строки cartridge, можно запустить один или несколько экземпляров:

$ cartridge start [APP_NAME[.INSTANCE_NAME]] [options]

Возможные параметры:

--script FILE

Точка входа в приложение. По умолчанию:

  • TARANTOOL_SCRIPT, либо
  • ./init.lua, если запуск идет из каталога приложения, или же
  • :путь_к_приложениям/:имя_приложения/init.lua в среде с несколькими приложениями.
--apps_path PATH
Путь к каталогу с приложениями при запуске из среды с несколькими приложениями. По умолчанию: /usr/share/tarantool.
--run_dir DIR
Каталог с файлами pid и sock. По умолчанию: TARANTOOL_RUN_DIR или /var/run/tarantool.
--cfg FILE
Файл конфигурации в формате YAML для экземпляра Cartridge. По умолчанию: TARANTOOL_CFG или ./instances.yml. Файл instances.yml содержит параметры cartridge.cfg(), которые описаны в разделе о конфигурации.
--foreground
Не в фоне.

Например:

$ cartridge start my_app --cfg demo.yml --run_dir ./tmp/run --foreground

Это запустит все экземпляры Tarantool, указанные в файле cfg, не в фоновом режиме с принудительным использованием переменных окружения.

Если APP_NAME не указано, cartridge выделит его из имени файла ./*.rockspec.

Если ИМЯ_ЭКЗЕМПЛЯРА не указывается, cartridge прочитает файл cfg и запустит все указанные экземпляры:

$ # в директории приложения
$ cartridge start # запускает все экземпляры
$ cartridge start .router_1 # запускает один экземпляр

$ # в среде с несколькими приложениями
$ cartridge start my_app # запускает все экземпляры приложения my_app
$ cartridge start my_app.router # запускает один экземпляр

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

$ cartridge stop [APP_NAME[.INSTANCE_NAME]] [options]

Поддерживаются следующие параметры из команды cartridge start:

  • --run_dir DIR
  • --cfg FILE

  • Чтобы запустить отдельный экземпляр:

    $ systemctl start APP_NAME
    

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

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

    $ systemctl start APP_NAME@INSTANCE_1
    $ systemctl start APP_NAME@INSTANCE_2
    ...
    $ systemctl start APP_NAME@INSTANCE_N
    

    где APP_NAME@INSTANCE_N – это имя экземпляра сервиса systemd с инкрементным числом N (уникальным для каждого экземпляра), которое следует добавить к порту 3300 для настройки прослушивания (например, 3301, 3302 и т.д.).

  • Чтобы остановить все сервисы на сервере, используйте команду systemctl stop и укажите имена экземпляров по одному. Например:

    $ systemctl stop APP_NAME@INSTANCE_1 APP_NAME@INSTANCE_2 ... APP_NAME@INSTANCE_<N>
    

При запуске экземпляров с помощью systemctl следует помнить о следующих правилах:

  • Можно задать конфигурацию экземпляра в YAML-файле.

    В этом файле могут быть эти параметры; см. пример здесь).

    Сохраните этот файл в /etc/tarantool/conf.d/ (путь по умолчанию для systemd) или в место, указанное в переменной окружения TARANTOOL_CFG (если вы отредактировали файл systemd в своем приложении). Имя файла не имеет значения — может быть instances.yml или любое другое, которое вам нравится.

    Вот что systemd делает дальше:

    • получает app_nameinstance_name, если оно указано) из имени файла systemd (например, APP_NAME@default или APP_NAME@INSTANCE_1);
    • задает значения по умолчанию для сокета консоли (например, /var/run/tarantool/APP_NAME@INSTANCE_1.control), PID-файла (например, /var/run/tarantool/APP_NAME@INSTANCE_1.pid) и параметра workdir (например, /var/lib/tarantool/<APP_NAME>.<INSTANCE_NAME>). Environment=TARANTOOL_WORKDIR=${workdir}.%i

    Наконец, cartridge ищет раздел с соответстующим названием (например, app_name, который содержит общую конфигурацию для всех экземпляров, и app_name.instance_1, который содержит конфигурацию для конкретного экземпляра) по всем YAML-файлам в /etc/tarantool/conf.d. В результате параметры Cartridge workdir, console_sock и pid_file в YAML-файле cartridge.cfg будут бесполезны, потому что systemd переопределит их.

  • Для запросов по журналам по умолчанию используется journalctl. Например:

    $ # показать журнал сообщений для юнит-файла systemd с именем APP_NAME.INSTANCE_1
    $ journalctl -u APP_NAME.INSTANCE_1
    
    $ # показать только самые последние сообщения и затем продолжать выводить новые
    $ journalctl -f -u APP_NAME.INSTANCE_1
    

    Если действительно нужно, можно изменить связанные с журналированием параметры box.cfg в конфигурационном YAML-файле: см. log и другие необходимые параметры.

Почти все ошибки в Cartridge создаются по формату return nil, err, где err — объект ошибки, созданный модулем errors. Cartridge не выдает ошибки, за исключением ошибок кода и несоответствия контрактов функций. При разработке новых ролей также нужно следовать этим рекомендациям.

Классы ошибок помогают обнаружить источник проблемы. Для этого объект ошибки содержит имя своего класса, трассировку стека и сообщение.

local errors = require('errors')
local DangerousError = errors.new_class("DangerousError")

local function some_fancy_function()

    local something_bad_happens = true

    if something_bad_happens then
        return nil, DangerousError:new("Oh boy")
    end

    return "success" -- недостижимо из-за ошибки
end

print(some_fancy_function())
nil DangerousError: Oh boy
stack traceback:
    test.lua:9: in function 'some_fancy_function'
    test.lua:15: in main chunk

Для равномерной обработки ошибок в errors есть API :pcall:

local ret, err = DangerousError:pcall(some_fancy_function)
print(ret, err)
nil DangerousError: Oh boy
stack traceback:
    test.lua:9: in function <test.lua:4>
    [C]: in function 'xpcall'
    .rocks/share/tarantool/errors.lua:139: in function 'pcall'
    test.lua:15: in main chunk
print(DangerousError:pcall(error, 'what could possibly go wrong?'))
nil DangerousError: what could possibly go wrong?
stack traceback:
    [C]: in function 'xpcall'
    .rocks/share/tarantool/errors.lua:139: in function 'pcall'
    test.lua:15: in main chunk

Для errors.pcall нет никакой разницы между return nil, err и error().

Обратите внимание, что API errors.pcall отличается от ванильного pcall в Lua. Вместо true первый возвращает значения, полученные в результате вызова. Если произошла ошибка, то вместо false вернется nil и сообщение об ошибке.

Удаленные вызовы net.box не сохраняют трассировку стека от удаленного устройства. В этом случае на помощь придет errors.netbox_eval. Он найдет трассировку стека с локального и удаленного хостов и восстановит метатаблицы.

> conn = require('net.box').connect('localhost:3301')
> print( errors.netbox_eval(conn, 'return nil, DoSomethingError:new("oops")') )
nil     DoSomethingError: oops
stack traceback:
        eval:1: in main chunk
during net.box eval on localhost:3301
stack traceback:
        [string "return print( errors.netbox_eval("]:1: in main chunk
        [C]: in function 'pcall'

Тем не мене, реализованный в Tarantool vshard не использует модуль errors. Вместо этого он использует собственные ошибки. Имейте это в виду при работе с функциями vshard.

Данные, включенные в объект ошибки (имя класса, сообщение, обратная трассировка), можно легко преобразовать в строку с помощью функции tostring().

В реализации GraphQL в Cartridge оборачивается модуль errors, поэтому обычно ответ на ошибку выглядит следующим образом:

{
    "errors":[{
        "message":"what could possibly go wrong?",
        "extensions":{
            "io.tarantool.errors.stack":"stack traceback: ...",
            "io.tarantool.errors.class_name":"DangerousError"
        }
    }]
}

Подробнее об ошибках читайте в спецификации GraphQL.

Если вы собираетесь реализовать обработчик GraphQL, то можете добавить свое расширение таким образом:

local err = DangerousError:new('I have extension')
err.graphql_extensions = {code = 403}

Ответ будет таким:

{
    "errors":[{
        "message":"I have extension",
        "extensions":{
            "io.tarantool.errors.stack":"stack traceback: ...",
            "io.tarantool.errors.class_name":"DangerousError",
            "code":403
        }
    }]
}

Если кратко, то объект errors представляет собой таблицу, то есть его можно быстро представить в JSON. В Cartridge это используется для обработки ошибок через HTTP:

local err = DangerousError:new('Who would have thought?')

local resp = req:render({
    status = 500,
    headers = {
        ['content-type'] = "application/json; charset=utf-8"
    },
    json = json.encode(err),
})
{
    "line":27,
    "class_name":"DangerousError",
    "err":"Who would have thought?",
    "file":".../app/roles/api.lua",
    "stack":"stack traceback:..."
}

Каждый экземпляр в кластере имеет встроенную машину состояний (state machine, конечный автомат). Это помогает управлять работой кластера и позволяет упростить описание распределенной системы.

../../../_images/state-machine.svg

Жизненный цикл экземпляра начинается с вызова cartridge.cfg. Во время инициализации экземпляр Cartridge привязывает TCP- (iproto) и UDP-сокеты (SWIM) и проверяет рабочую директорию. В зависимости от результата экземпляр переходит в одно из следующих состояний:

../../../_images/InitialState.svg

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

Экземпляр начинает принимать запросы iproto (бинарный протокол Tarantool) и остается в этом состоянии до тех пор, пока пользователь не решит присоединить его к кластеру — создать набор реплик или присоединиться к существующему.

После этого экземпляр переходит в состояние BootstrappingBox.

../../../_images/Unconfigured.svg

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

../../../_images/ConfigFound.svg

Файл конфигурации был найден, загружен и проверен. Следующий шаг — конфигурация экземпляра. Если были обнаружены прошлые снимки данных, состояние экземпляра изменится на RecoveringSnapshot. В противном случае экземпляр перейдет в состояние BootstrappingBox. По умолчанию все экземпляры запускаются в режиме только для чтения и не принимают запросы до завершения загрузки или восстановления.

../../../_images/ConfigLoaded.svg

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

  • Возникла ошибка во время соединения cartridge.remote-control с бинарным портом;
  • В рабочей директории (tmp/) отсутствует config.yml, хотя снимки данных есть;
  • Ошибка при загрузке конфигурации с диска;
  • Некорректная конфигурация: в конфигурации кластера отсутствует сервер.

Если снимки данных или файлы конфигурации отсутствуют, выполняется настройка аргументов для box.cfg, затем выполняется сам box.cfg. Также на этом этапе происходит настройка пользователей и остановка remote-control. Экземпляр попытается начать прослушивать полнофункциональный протокол iproto. В случае неудачной попытки экземпляр изменит свое состояние на BootError. Если всё в порядке, экземпляр примет состояние ConnectingFullmesh. Если в конфигурации на уровне кластера нет набора реплик, экземпляр также перейдет в состояние BootError.

../../../_images/Recovery.svg

Если имеются снимки данных, box.cfg начнет процесс восстановления. После этого ход действий аналогичен BootstrappingBox.

Это состояние может быть вызвано следующими событиями:

  • Не удалось выполнить привязку к двоичному порту для использования iproto;
  • В конфигурации на уровне кластера отсутствует сервер;
  • В конфигурации на уровне кластера отсутствует набор реплик;
  • Не удалось настроить репликацию.

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

../../../_images/ConnectingFullmesh.svg

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

Состояние конфигурации ролей. Экземпляр попадает в это состояние при начальной настройке, при запуске восстановления после сбоя (failover.lua) или при изменении конфигурации на уровне кластера (twophase.lua).

../../../_images/ConfiguringRoles.svg

Роли были успешно сконфигурированы.

Ошибка при конфигурировании ролей.