Хабрахабр

Тarantool Cartridge: шардирование Lua-бекенда в три строчки

Он быстрый и классный, но возможности одного сервера всё равно не безграничны. У нас в Mail.ru Group есть Tarantool — это такой сервер приложений на Lua, который по совместительству ещё и база данных (или наоборот?). Он позволяет шардировать данные по нескольким серверам, но придётся повозиться, чтобы его настроить и прикрутить бизнес-логику. Вертикальное масштабирование тоже не панацея, поэтому в Tarantool есть инструменты для горизонтального масштабирования — модуль vshard [1].

Хорошие новости: мы собрали шишек (например [2], [3]) и запилили очередной фреймворк, который заметно упростит решение этой проблемы.

Он позволяет сфокусироваться на написании бизнес-логики вместо решения инфраструктурных проблем. Тarantool Cartridge — это новый фреймворк для разработки сложных распределённых систем. Пот катом я расскажу, как этот фреймворк устроен и как с его помощью писать распределённые сервисы.

А в чём, собственно, проблема?

У нас есть тарантул, есть vshard — чего ещё пожелать?

Конфигурация vshard настраивается через Lua-таблицы. Во-первых, дело в удобстве. Никто не хочет заниматься этим вручную. Чтобы распределённая система из нескольких процессов Tarantool работала правильно, конфигурация должна везде быть одинаковой. Поэтому в ход идут всяческие скрипты, Ansible, системы развёртывания.

По сути, это простой YAML-файл, копия которого хранится в каждом экземпляре Tarantool. Cartridge сам управляет конфигурацией vshard, он делает это на основе своей собственной распределённой конфигурации. Упрощение заключается в том, что фреймворк сам следит за своей конфигурацией и за тем, чтобы она везде была одинаковая.

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

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

Роли — это та концепция, которая позволяет сфокусироваться разработчику на написании кода. Cartridge вводит понятие роли для каждого процесса Tarantool. Все имеющиеся в проекте роли можно запустить на одном экземпляре Tarantool, и для тестов этого будет достаточно.

Основные возможности Tarantool Cartridge:

  • автоматизированное оркестрирование кластера;
  • расширение функциональности приложения с помощью новых ролей;
  • шаблон приложения для разработки и развертывания;
  • встроенное автоматическое шардирование;
  • интеграция с тестовым фреймворком Luatest;
  • управление кластером с помощью WebUI и API;
  • инструменты упаковки и деплоя.

Hello, World!

Мне не терпится показать сам фреймворк, поэтому рассказ про архитектуру оставим на потом, и начнём с простого. Если предположить, что сам Tarantool уже установлен, то остаётся сделать только.

$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH

Эти две команды установят утилиты командной строки и позволят создать своё первое приложение из шаблона:

$ cartridge create --name myapp

И вот что мы получим:

myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│ ├── helper
│ │ ├── integration.lua
│ │ └── unit.lua
│ ├── helper.lua
│ ├── integration/api_test.lua
│ └── unit/sample_test.lua
└── tmp/

Это git-репозиторий с готовым «Hello, World!» приложением. Давайте сразу попробуем его запустить, предварительно установив зависимости (в т.ч. сам фреймворк):

$ tarantoolctl rocks make
$ ./init.lua --http-port 8080

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

Разработка приложений

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

Прорабатываем архитектуру дальше. Мы начинаем рисовать схему, и помещаем на неё три компонента: gateway, storage и scheduler. Ни gateway, ни scheduler обращаться в хранилище напрямую не будут, для этого есть роутер, он для того и создан. Раз мы используем в качестве хранилища vshard, то добавляем в схему vshard-router и vshard-storage.

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

Зачем нам лишний раз ходить по сети, если это и так входит в обязанности роутера? Держать vshard-router и gateway на отдельных экземплярах смысла мало. То есть в в одном процессе инициализируюстя и gateway, и vshard.router.cfg, и пусть они взаимодействуют локально. Они должны быть запущены внутри одного процесса.

Мне нужно запустить тесты и проверить, что я правильно написал gateway. На этапе проектирования работать с тремя компонентами было удобно, но я, как разработчик, пока пишу код, не хочу задумываться о запуске трёх экземпляров Tarnatool. Зачем мне мучиться с развёртыванием трёх экземпляров? Или, может, я хочу продемонстрировать коллегам фичу. Роль — это обычный луашный модуль, жизненным циклом которого управляет Cartridge. Именно так родилась концепция ролей. В другом проекте их может быть больше. В данном примере их четыре — gateway, router, storage, scheduler. Все роли можно запустить в одном процессе, и этого будет достаточно.

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

Управление топологией

Информацию о том, где какие роли запущены, надо где-то хранить. И это «где-то» — распределённая конфигурация, о которой я уже упоминал выше. Самое главное в ней — это топология кластера. Здесь изображено 3 репликационные группы из 5 процессов Tarantool:

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

Обычно это тот экземпляр, на который идёт запись. Также в секции топологии указан такой важный параметр, как лидер каждой репликационной группы. Иногда смелые разработчики не боятся конфликтов и могут писать данные на несколько реплик параллельно, но есть некоторые операции, которые несмотря ни на что не должны выполняться дважды. Остальные чаще всего являются read-only, хотя тут могут быть исключения. Для этого есть признак лидера.

Жизнь ролей

Чтобы абстрактная роль могла существовать в такой архитектуре, фреймворк должен ими как-то управлять. Естественно, управление происходит без перезапуска процесса Tarantool. Для управления ролями существует 4 колбека. Cartridge сам будет их вызвать в зависимости от того, что у него написано в распределённой конфигурации, тем самым применяя конфигурацию к конкретным ролям.

function init()
function validate_config()
function apply_config()
function stop()

У каждой роли есть функция init. Она вызывается один раз либо при включении роли, либо при перезапуске Tarantool’а. Там удобно, например, инициализировать box.space.create, или scheduler может запустить какой-нибудь фоновый fiber, который будет выполнять работу через определённые интервалы времени.

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

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

Если мы говорим, что scheduler на этом сервере больше не нужен, он может остановить те файберы, которые запускал с помощью init. Также у ролей есть метод stop, который нужен для очистки результатов жизнедеятельности роли.

Мы привыкли писать вызовы функций на Lua, но может случиться так, что в данном процессе нет нужной нам роли. Роли могут взаимодействовать между собой. Это может пригодиться, если, например, ваш gateway захочет напрямую попросить scheduler сделать работу прямо сейчас, а не ждать сутки. Чтобы облегчить обращения по сети, мы используем вспомогательный модуль rpc (remote procedure call), который построен на основе стандартного netbox, встроенного в Tarantool.

Для мониторинга здоровья в Cartridge используется протокол SWIM [4]. Ещё один важный момент — обеспечение отказоустойчивости. Если вдруг ответ не пришёл, Tarantool начинает подозревать что-то неладное, а через некоторое время декламирует смерть и начинает рассказывает всем окружающим эту новость. Если говорить вкратце, то процессы обмениваются друг с другом «слухами» по UDP — каждый процесс рассказывает своим соседям последние новости, и они отвечают.

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

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

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

Наши роли не настолько изолированы, как Docker-контейнеры. В отличие от Docker, мы не можем просто взять «образ» роли, отнести его на другую машину и там запустить. Роль либо есть, либо её нет, в каком-то смысле это singleton. Также мы не можем запустить на одном экземпляре две одинаковые роли. Ну и в-третьих, внутри всей репликационной группы роли должны быть одинаковыми, потому что иначе было бы нелепо — данные одинаковые, а конфигурация разная.

Инструменты деплоя

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

$ cartridge pack rpm myapp -- упакует для нас ./myapp-0.1.0-1.rpm
$ sudo yum install ./myapp-0.1.0-1.rpm

Установленный пакет несёт в себе почти всё необходимое: и приложение, и установленные луашные зависимости. Tarantool на сервер тоже приедет как зависимость RPM-пакета, и наш сервис готов к запуску. Делается это через systemd, но прежде необходимо написать немного конфигурации. Как минимум, указать URI каждого процесса. Трёх для примера хватит.

$ sudo tee /etc/tarantool/conf.d/demo.yml <<CONFIG
myapp.router:
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIG

Здесь есть интересный нюанс. Вместо того, чтобы указать лишь порт бинарного протокола, мы указываем публичный адрес процесса целиком включая hostname. Это нужно для того, чтобы узлы кластера знали, как друг с другом соединиться. Плохая идея использовать в качестве advertise_uri адрес 0.0.0.0, это должен быть внешний IP-адрес, а не bind сокета. Без него ничего работать не будет, поэтому Cartridge попросту не даст запустить узел с неправильным advertise_uri.

Так как обычный systemd-юнит не позволяет стартовать больше одного процесса, приложения на Cartridge устанавливает т.н. Теперь, когда конфигурация готова, можно запустить процессы. instantiated-юниты, которые работают так:

$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_B

В конфигурации мы указали HTTP-порт, на котором Cartridge обслуживает веб-интерфейс — 8080. Зайдём на него и посмотрим:

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

Приложение можно эксплуатировать. Нальём кружечку любимого напитка и расслабимся после долгой рабочей недели.

Итоги

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

Ссылки

[1] Tarantool » 2.2 » Reference » Rocks reference » Module vshard

[2] Как мы внедряли ядро инвестиционного бизнеса Альфа-Банка на базе Tarantool

[3] Архитектура биллинга нового поколения: трансформация с переходом на Tarantool

[4] SWIM — протокол построения кластера

[5] GitHub — tarantool/cartridge-cli

[6] GitHub — tarantool/cartridge

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть