Top.Mail.Ru
Руководство разработчика | Tarantool
 
Tarantool Cartridge / Руководство разработчика
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 options example
  workdir = '/var/lib/tarantool/app',
  advertise_uri = 'localhost:3301',
  cluster_cookie = 'super-cluster-cookie',
  ...
}, {
-- box options example
  memtx_memory = 1000000000,
  ... })
 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Примечание

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

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

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

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

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

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

    -- Implement a custom role in 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:

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

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

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

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

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

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

    Примечание

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

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

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

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

Например:

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

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

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

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

-- Specify groups in 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 vhsard = require('vshard')
vshard.router.call(...)

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

local router_role = cartridge.service_get('vshard-router')
local default_router = router_role.get() -- or 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.

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

Вы можете:

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

    # in YAML configuration file
    my_role:
      notify_url: "https://localhost:8080"
    
    -- in init.lua file
    local notify_url = 'http://localhost'
    function my_role.apply_config(conf, opts)
      local conf = conf['my_role'] or {}
      notify_url = conf.notify_url or 'default'
    end
    
  • Загружать и выгружать конфигурацию всего кластера через веб-интерфейс или с помощью 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():

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

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

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

    -- Inside the apply_config() function:
    
    if opts.is_master then
    
        -- Here you can call box functions
    
    end
    

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

-- Custom role implementation

local cartridge = require('cartridge')

local role_name = 'custom-role'

-- Modify the config by implementing some setter (an alternative to HTTP PUT)
local function set_secret(secret)
    local custom_role_cfg = cartridge.confapplier.get_deepcopy(role_name) or {}
    custom_role_cfg.secret = secret
    cartridge.confapplier.patch_clusterwide({
        [role_name] = custom_role_cfg,
    })
end
-- Validate
local function validate_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    if custom_role_cfg.secret ~= nil then
        assert(type(custom_role_cfg.secret) == 'string', 'custom-role.secret must be a string')
    end
    return true
end
-- Apply
local function apply_config(cfg)
    local custom_role_cfg = cfg[role_name] or {}
    local secret = custom_role_cfg.secret or 'default-secret'
    -- Make use of it
end

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

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

В примере реализации можно вызвать функцию 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()). Можно привязать порт к экземпляру через переменную окружения:

-- Get the port from an environmental variable or the default one:
local http_port = os.getenv('HTTP_PORT') or '8080'

local ok, err = cartridge.cfg({
   ...
   -- Pass the port to the cluster:
   http_port = http_port,
   ...
})

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

local function init(opts)

...

   -- Get the httpd instance:
   local httpd = cartridge.service_get('httpd')
   if httpd ~= nil then
       -- Configure a route to, for example, metrics:
       httpd:route({
               method = 'GET',
               path = '/metrics',
               public = true,
           },
           function(req)
               return req:render({json = stat.stat()})
           end
       )
   end
end

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

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

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

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

    -- auth.lua
    
    -- Add a function to check the credentials
    local function check_password(username, password)
    
        -- Check the credentials any way you like
    
        -- Return an authentication success or failure
        if not ok then
            return false
        end
        return true
    end
    ...
    
  2. Передайте имя используемого модуля auth в качестве параметра для cartridge.cfg(), чтобы кластер мог использовать его:

    -- init.lua
    
    local ok, err = cartridge.cfg({
        auth_backend_name = 'auth',
        -- The cluster will automatically call 'require()' on the 'auth' module.
        ...
    })
    

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

    Примечание

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

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

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

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

В Tarantool Cartridge используется семантический подход к управлению версиями, как описано на сайте 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
        -- validate tube_opts
    end
    return true
end

local function apply_config(conf, opts)
    if opts.is_master then
        local tubes = cfg.tubes or {}
        -- create tubes according to the configuration
    end
    return true
end

В 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]
    $ # -- OR --
    $ 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
    $ # -- OR --
    $ sudo dpkg -i <APP_NAME>-<VERSION>.deb
    
  4. Настройте экземпляры приложения.

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

    Например:

    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 # starts a single instance
      
    • или cartridge, например:

      # in application directory
      $ cartridge start # starts all instances
      $ cartridge start .router_1 # starts a single instance
      
      # in multi-application environment
      $ cartridge start my_app # starts all instances of my_app
      $ cartridge start my_app.router # starts a single instance
      
  6. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

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

    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 # starts a single instance
      
    • или cartridge, например:

      # in application directory
      cartridge start # starts all instances
      cartridge start .router_1 # starts a single instance
      
      # in multi-application environment
      cartridge start my_app # starts all instances of my_app
      cartridge start my_app.router # starts a single instance
      
  4. Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.

    Примечание

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

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

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

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

$ tarantool init.lua # the simplest command

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

Чтобы остановить экземпляр, используйте 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 и запустит все указанные экземпляры:

# in application directory
cartridge start # starts all instances
cartridge start .router_1 # start single instance

# in multi-application environment
cartridge start my_app # starts all instances of my_app
cartridge start my_app.router # start a single instance

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

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

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

  • --run_dir DIR
  • --cfg FILE

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

    $ systemctl 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. Например:

    # show log messages for a systemd unit named APP_NAME.INSTANCE_1
    $ journalctl -u APP_NAME.INSTANCE_1
    
    # show only the most recent messages and continuously print new ones
    $ journalctl -f -u APP_NAME.INSTANCE_1
    

    Если действительно нужно, можно изменить связанные с журналированием параметры 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" -- not reachable due to the error
end

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

Для равномерной обработки ошибок в 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:..."
}