
В Mail.ru Group имаме Tarantool — това е приложение-сървър на Lua, което също така функционира и като база данни (или обратното?). Той е бърз и чудесен, но възможностите на един сървър не са безкрайни. Вертикалното мащабиране също не е панацея, затова в Tarantool има инструменти за хоризонтално мащабиране — модулът vshard. . Той позволява шардирне на данни между няколко сървъра, но ще се наложи да се поизпотите, за да го настроите и свържете бизнес логиката.
Добри новини: събрахме опит (например , ) и разработихме нов фреймворк, който значително улеснява решаването на този проблем.
— това е нов фреймворк за разработка на сложни разпределени системи. Той позволява да се съсредоточите върху писането на бизнес логика вместо да се занимавате с инфраструктурни проблеми. Под кат ще разкажа как е устроен този фреймворк и как с негова помощ да пишете разпределени услуги.
А как всъщност е проблемът?
Ние имаме тарантул, имаме vshard — какво още желаем?
На първо място, става въпрос за удобство. Конфигурацията на vshard се настройва чрез Lua-таблици. За да работи правилно разпределената система от няколко процеса Tarantool, конфигурацията навсякъде трябва да бъде еднаква. Никой не иска да се занимава с това на ръка. Затова се използват различни скриптове, Ansible, системи за разгръщане.
Cartridge сам управлява конфигурацията на vshard, той прави това на базата на своята собствена разпределена конфигурация. По същество, това е прост YAML-файл, чиято копия се съхранява във всеки екземпляр на Tarantool. Улеснението се състои в това, че фрейм워크 сам следи своята конфигурация и за да бъде навсякъде еднаква.
На второ място, отново става въпрос за удобство. Конфигурацията на 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И така, стартирахме една нода от бъдещото шардизирано приложение. Любопитният потребител може веднага да отвори уеб интерфейса, да конфигурира клъстера от един узел с мишката и да се наслади на резултата, но радостта е преждевременна. Все още приложението не умеe да прави нищо полезно, затова за деплоя ще говоря по-късно, а сега е време да пишем код.
Разработка на приложения
Представете си, че проектираме проект, който трябва да приема данни, да ги записва и веднъж на ден да генерира отчет.

Започваме да рисуваме схемата и слагаме три компонента: gateway, storage и scheduler. Разработваме архитектурата по-нататък. Тъй като използваме vshard за хранилище, добавяме в схемата vshard-router и vshard-storage. Нито gateway, нито scheduler ще се свържат с хранилището директно, за това има роутер, който е именно за това.

Тази схема все още не напълно точно отразява това, което ще създадем в проекта, тъй като компонентите изглеждат абстрактно. Трябва да видим как това ще се проектира на реалния Tarantool — ще групираме нашите компоненти по процеси.

Няма смисъл да държим vshard-router и gateway на отделни инстанции. Защо да обикаляме мрежата, ако това е в задълженията на роутера? Те трябва да бъдат стартирани в един и същи процес. Тоест, в един и същи процес да се инициализират и gateway, и vshard.router.cfg, и нека те да взаимодействат локално.
На етапа на проектиране беше удобно да работим с три компонента, но аз, като разработчик, докато пиша код, не искам да мисля за стартиране на три инстанции на Tarantool. Трябва ми да стартирам тестове и да проверя дали правилно съм написал gateway. Или може би искам да демонстрирам функция на колегите. Защо да се мъча с разгръщането на три инстанции? Именно така се роди концепцията за роли. Ролята е обикновен луашен модул, чийто жизнен цикъл се управлява от 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, който да извършва работа на определени интервали.
Една функция init може да бъде недостатъчна. Cartridge позволява на ролите да използват дистрибираната конфигурация, която той използва за съхранение на топологията. В тази конфигурация можем да обявим нова секция и да съхраняваме в нея фрагмент от бизнес конфигурацията. В моя пример това може да бъде схема на данни или настройки за расписание за ролята на scheduler.
Кластерът извиква validate_config и apply_config при всяка промяна на дистрибираната конфигурация. Когато конфигурацията се прилага с двуфазен комит, кластерът проверява дали всяка роля е готова да приеме новата конфигурация и при необходимост уведомява потребителя за грешка. Когато всички се съгласят, че конфигурацията е нормална, се изпълнява apply_config.
Също така, ролите имат метод stop, който е необходим за почистване на резултатите от дейността на ролята. Ако казваме, че scheduler на този сървър вече не е нужен, той може да спре тези fibers, които е стартирал с помощта на init.
Ролите могат да взаимодействат помежду си. Свикнали сме да пишем извиквания на функции на Lua, но може да се случи така, че в този процес да няма необходимата роля. За да улесним мрежовите повиквания, използваме помощния модул rpc (remote procedure call), който е построен на базата на стандартния netbox, вграден в Tarantool. Това може да бъде полезно, ако, например, вашият gateway иска директно да поиска от scheduler да извърши работа в момента, а не да чака 24 часа.
Още един важен момент — осигуряване на отказоустойчивост. За мониторинг на здравето в Cartridge се използва протоколът SWIM. . Накратко, процесите обменят помежду си "слухове" по UDP — всеки процес разказва на своите съседи последните новини, и те отговарят. Ако случайно отговор не пристигне, Tarantool започва да подозира, че нещо не е наред, и след известно време обявява смърт и започва да разказва на всички около себе си тази новина.

На базата на този протокол 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Инсталираният пакет съдържа почти всичко необходимо: и приложението, и инсталираните lua зависимости. Tarantool също ще дойде на сървъра като зависимост на RPM пакета, и нашата услуга е готова за стартиране. Това се прави чрез systemd, но преди това е необходимо да напишем малко конфигурация. Най-малкото, да посочим URI на всеки процес. Трима за пример ще са достатъчни.
$ sudo tee \/etc\/tarantool\/conf.d\/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIGТук има интересен нюанс. Вместо да посочим само порта за бинарния протокол, посочваме публичния адрес на процеса в цялост, включително hostname. Това е необходимо, за да могат възлите в клъстера да знаят как да се свързват помежду си. Лоша идея е да използвате адрес 0.0.0.0 като advertise_uri, това трябва да е външният 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. Нека го посетим и да видим:

Виждаме, че процесите са стартирани, но все още не са конфигурирани. Картриджът не знае кой с кого трябва да репликира и не може сам да вземе решение, затова чака нашите действия. А нашият избор не е с голям обхват: животът на новия кластер започва с конфигурацията на първия узел. После ще добавим останалите към кластера, ще им назначим роли и тогава можем да считаме разгръщането за успешно завършено.
Налейте си чаша любимата напитка и се отпуснете след дълга работна седмица. Приложението може да бъде експлоатирано.

Итог
Какви са резултатите? Опитайте, използвайте, оставяйте обратна връзка, създавайте тикети в GitHub.
Връзки
[1]
[2]
[3]
[4]
[5]
[6]
Източник: habr.com
