Привет! Я продолжаю разрабатывать распределённые системы на основе Tarantool. За последний год наша команда вывела в прод 17 новых систем. В прошлый раз я рассказал, как мы наладили автоматический деплой. В этой статье я покажу, как упростить обслуживание приложений на Tarantool Cartridge.

У одного крупного заказчика процесс выхода в прод сложный. Софт прогоняют через длинный чек-лист и, если система чему-то не соответствует, то никакого прода. Вместе с системой мы должны предоставить документацию по «эксплуатации в штатном режиме» и «траблшутингу» на случай аварий. Одной только документации мало, должно быть обучение! Взять хотя бы общий сценарий «как восстановить упавшую реплику Tarantool» — мы должны показать команде эксплуатации последовательность шагов и ответить на вопросы.

«Общетарантульные» сценарии не меняются от приложения к приложению. А есть специфичные операции: перезагрузить данные кэша, сформировать отчёт по рекламной кампании и т. д. Каждая система будет иметь свой набор. Запуском этих сценариев тоже занимается команда эксплуатации, и тоже необходимо обучение. Однако у нас возникла проблема с доставкой таких операций до конечного исполнителя.

Мы испробовали разные способы:

  • Скрипт. Можно подложить в поставку с приложением. Плохая масштабируемость между разными приложениями и командами. Каждый разработчик захочет написать свой скрипт для вкручивания лампочки.
  • HTTP API. По сути — удалённый способ вызова встроенной функции. Требует расширенной документации и способов использования. Сойдёт для первой реализации.
  • Вместе с инструментом автоматизации. Например, отдельные ansible-playbook для работы с теми же HTTP-вызовами. Это удобнее с точки зрения эксплуатации, но требует повышенной осторожности и документации. Кажется слишком громоздким решением.
  • Утилита для управления приложением. Единая точка входа для набора скриптов со встроенной документацией, возможными аргументами и сценариями использования. Можно реализовать --help для получения справки и всех доступных команд. Лучше переиспользуется, однако не защищает от копирования и создания по одной утилите на каждый проект.

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

Тут появляется cartridge admin, который призван упростить для разработчика написание и поддержку эксплуатационных кейсов, повысить переиспользование операций, оптимизировать доставку до эксплуатации. Больше не нужно выдумывать, куда приложить кейс и как описать его в документации. cartidge admin становится единым местом описания всех процедур обслуживания. Эксплуатация становится частью кода, её можно обложить тестами и гарантировать работоспособность.

С точки зрения команды эксплуатации ситуация аналогична. Есть 17 проектов: каждый имеет свою специфику, документацию и скрипты. Нужно постоянно переключаться между контекстами: WebUI, tarantoolctl, cartridge, HTTP API и т.д. Чем больше разных вызовов, тем больше вероятность ошибки.

И здесь cartridge admin становится единой точкой взаимодействия. Утилита имеет простой консольный интерфейс и обратную связь. При обслуживании приложений на Tarantool Cartridge не придётся переключать контекст. Вызов справки позволяет ознакомиться со всеми операциями, доступными для конкретной системы. Риск человеческой ошибки минимизируется.

Далее опишу как работать с cartridge admin.

Необходимые компоненты



Установим cartridge-cli.

Для Centos:

$ curl -L https://tarantool.io/installer.sh | sudo bash -s -- --repo-only

$ sudo yum install cartridge-cli

Для MacOS:

$ brew install cartridge-cli

Альтернативно, можно использовать наше энтерпрайз-решение Tarantool Enterprise SDK, который уже содержит нужные версии компонентов и сам Tarantool. Также в составе идет скрипт для настройки окружения — env.sh.

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


Для простоты возьмём проект, созданный с помощью cartridge create (инструкция).

$ cartridge create --name tarantool-demo
$ cd tarantool-demo
$ cartridge build

Настраиваем сборку артефакта


  • В файле проекта tarantool-demo-scm-1.rockspec нужно добавить зависимость cartridge-cli-extensions.

    dependencies = {
        ...
        'cartridge-cli-extensions == 1.1.0-1',
        ...
    }
    

  • В cartridge.pre-build добавить копирование бинаря cartridge, так как он не подкладывается в сборку автоматически. Например, если SDK был распакован в одноименную директорию:

    #!/usr/bin/env bash
    
    cp `which cartridge` .
    

    Теперь артефакт будет содержать все нужные модули.

    $ cartridge pack tgz --version 1
    

Пишем admin функцию


Вначале предлагаю ознакомиться с документацией README, которая находится в репозитории cartridge-cli-extension. Там пошагово описан процесс инициализации модуля и регистрации функций.

Из интересного:

  • В функции можно использовать команду print(). Сообщение сразу же будет напечатано в консоль. Удобно для промежуточных результатов выполнения команды, ошибок, прогресса.

    •  Cleanup: ready to proceed on 2 storages
    
       •  Cleanup [1/2  tnt-storage_2]: deleted {«products»:100}
    
       •  Cleanup [2/2  tnt-storage_1]: deleted {«products»:50}
    
       •  Deleted total {«products»:150}
    
    -------------------
    
       •  Cleanup: ready to proceed on 2 storages
    
       •  Cleanup [1/2  tnt-storage_2]: error NetboxEvalError: Peer closed
    

  • Консольный вывод поддерживает смайлики (utf-8)

Пример


Я хочу организовать чистку хранилища, а именно таблицы некоторых продуктов. Ниже описана admin функция, которая вызывает box.space.products:truncate() на подмножестве серверов (в данном случае стораджей-мастеров) и возвращает количество удаленных записей.

Для этого добавим в конец файла init.lua следующий код. В коде есть пояснения к каждой секции.

local admin_tasks = {}
local json = require('json')
local pool = require('cartridge.pool')

admin_tasks.cleanup = {
    -- описываем новую функцию
    usage = 'Clean old products from cache',
    -- конкретно cleanup не будет принимать аргументов.
    -- с форматом аргументов можно ознакомиться в примере на GitHub
    -- https://github.com/tarantool/cartridge-cli-extensions/blob/master/doc/admin.md#example
    args = nil, 
    call = function(opts)
        opts = opts or {}
        local servers = {}
        local total = { products = 0 }

        -- функция для проверки имеет ли репликасет определённую роль
        local function replicaset_has_role(replicaset, role)
            for _, name in ipairs(replicaset.roles) do
                if name == role then
                    return true
                end
            end
            return false
        end

        -- обходим все сервера в кластере и выбираем только те, которые:
        -- 1) имеют роль vshard-storage
        -- 2) являются мастером в своем репликасете

    for _, rs in pairs(cartridge.admin_get_replicasets()) do

            if replicaset_has_role(rs, 'vshard-storage') then
                local server = rs.active_master
                if server ~= nil and server ~= "expelled" then
                    if server.status == 'unreachable' then
                        return nil, ("Server %s status is %s"):format(server.alias, server.status)
                    else
                        servers[server.alias] = server.uri
                    end
                end
            end
        end

        -- выполняем очистку данных по очереди на каждом из стораджей
        for alias, uri in pairs(servers) do
            -- префикс можно задать любой, например добавить порядковый номер сервера
            local prefix = alias
            local conn, err = pool.connect(uri)
            if err ~= nil then
                print(("Cleanup [%s]: error %s"):format(prefix, err))
            else
                -- собственно вызов функции
                -- здесь есть небольшой хак, создадим таблицы прямо из admin функции
                -- на реальном примере таблица должна существовать изначально
                local res = conn:eval([[
                    box.schema.create_space('products', { if_not_exists = true })
                    box.space.products:create_index('primary', { if_not_exists = true })

                    local len = box.space.products:len()
                    box.space.products:truncate()
                    return { products = len }
                ]])
                -- выводим промежуточный результат в консоль
                print(("Cleanup [%s]: deleted %s"):format(prefix, json.encode(res)))
                total.products = total.products + res.products
            end
        end
        return ("Deleted total %s"):format(json.encode(total))
    end
}

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

Теперь зарегистрируем эту функции в кластере.

-- инициализация модуля admin
local cli_admin = require('cartridge-cli-extensions.admin')
cli_admin.init()

-- регистрируем все функции таблицы admin_tasks из примера выше
for name, task in pairs(admin_tasks) do
    local ok, err = cli_admin.register(name, task.usage, task.args, task.call)
    if ok ~= true then
        error(err)
    end
end

В коде есть создание таблицы прямо в admin-функции. Конечно на реальном примере так делать не нужно. Если запустить функцию без этих строк

box.schema.create_space('products', { if_not_exists = true })
box.space.products:create_index('primary', { if_not_exists = true })

то получим ошибку, так как таблицы products не существует.

? Failed to call "cleanup": eval:1: attempt to index field 'products' (a nil value)

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


$ cartridge start -d
$ cartridge replicasets setup

Запускаем admin функцию


Все вызовы необходимо производить из директории с бинарем cartridge-cli.

Начиная с cartridge-cli v2.6 есть два способа подключиться к инстансу Tarantool.

  • Через socket-файл

    ./cartridge admin   --name tarantool-demo   --run-dir ./tmp/run   --instance router   --list
    
  • С помощью логин-пароля по сети

    ./cartridge admin   --conn admin:<cluster-cookie>@localhost:3301
      --list
    

Далее буду рассматривать работу с socket-файлами. Оба способа идентичны в плане запуска команд.

Сначала проверим, какие функции определены в кластере. Используем директиву --list:

./cartridge admin   --name tarantool-demo   --run-dir ./tmp/run   --instance router   --list

  • Обязательно задать имя приложения через --name.
  • Директория для поиска socket-файлов --run-dir.
  • Имя инстанса в кластере задается с помощью --instance.
  • При указанных параметрах cartridge-cli будет работать с кластером через сокет tarantool-demo.router.control. Его наличие в run-dir обязательно. Для Tarantool Cartridge путь к сокету можно задать, например, через переменную среды TARANTOOL_CONSOLE_SOCK.

    Получаем в ответ:

    • Available admin functions:
    
    cleanup  Clean old products from cache
    

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

    ./cartridge admin   --name tarantool-demo   --run-dir ./tmp/run   --instance router   cleanup
    
  • Название команды (cleanup) и любые параметры, которые были определены для этой команды, указываются в конце.

И всё, по завершению операции кластер будет чист!

Итоги


cartridge admin оказался неплохой заменой разрозненной кучке скриптов в нашем окружении. Мы перевели часть операций на его рельсы и не собираемся останавливаться.

Появилась единая точка «сбора» всех функций по управлению системой. Упростились как технические, так и административные процессы. Меньше затрат на разработку и поддержку эксплуатационных сценариев. С помощью cartridge admin обслуживание приложения на Tarantool Cartridge стало удобнее и понятнее.